1
0
mirror of https://github.com/ClusterCockpit/cc-backend synced 2025-04-08 05:05:55 +02:00

Merge branch 'master' into hotfix

This commit is contained in:
Jan Eitzinger 2025-03-14 10:00:29 +01:00
commit 0adda4bf7b
39 changed files with 2289 additions and 1425 deletions

@ -137,6 +137,11 @@ type JobMetricWithName {
metric: JobMetric! metric: JobMetric!
} }
type JobMetricStatWithName {
name: String!
stats: MetricStatistics!
}
type JobMetric { type JobMetric {
unit: Unit unit: Unit
timestep: Int! timestep: Int!
@ -242,6 +247,7 @@ type Query {
job(id: ID!): Job job(id: ID!): Job
jobMetrics(id: ID!, metrics: [String!], scopes: [MetricScope!], resolution: Int): [JobMetricWithName!]! jobMetrics(id: ID!, metrics: [String!], scopes: [MetricScope!], resolution: Int): [JobMetricWithName!]!
jobMetricStats(id: ID!, metrics: [String!]): [JobMetricStatWithName!]!
jobsFootprints(filter: [JobFilter!], metrics: [String!]!): Footprints jobsFootprints(filter: [JobFilter!], metrics: [String!]!): Footprints
jobs(filter: [JobFilter!], page: PageRequest, order: OrderByInput): JobResultList! jobs(filter: [JobFilter!], page: PageRequest, order: OrderByInput): JobResultList!

@ -202,7 +202,7 @@
"200": { "200": {
"description": "Success message", "description": "Success message",
"schema": { "schema": {
"$ref": "#/definitions/api.DeleteJobApiResponse" "$ref": "#/definitions/api.DefaultJobApiResponse"
} }
}, },
"400": { "400": {
@ -272,7 +272,7 @@
"200": { "200": {
"description": "Success message", "description": "Success message",
"schema": { "schema": {
"$ref": "#/definitions/api.DeleteJobApiResponse" "$ref": "#/definitions/api.DefaultJobApiResponse"
} }
}, },
"400": { "400": {
@ -342,7 +342,7 @@
"200": { "200": {
"description": "Success message", "description": "Success message",
"schema": { "schema": {
"$ref": "#/definitions/api.DeleteJobApiResponse" "$ref": "#/definitions/api.DefaultJobApiResponse"
} }
}, },
"400": { "400": {
@ -487,7 +487,7 @@
"201": { "201": {
"description": "Job added successfully", "description": "Job added successfully",
"schema": { "schema": {
"$ref": "#/definitions/api.StartJobApiResponse" "$ref": "#/definitions/api.DefaultJobApiResponse"
} }
}, },
"400": { "400": {
@ -581,7 +581,7 @@
} }
}, },
"422": { "422": {
"description": "Unprocessable Entity: finding job failed: sql: no rows in result set", "description": "Unprocessable Entity: job has already been stopped",
"schema": { "schema": {
"$ref": "#/definitions/api.ErrorResponse" "$ref": "#/definitions/api.ErrorResponse"
} }
@ -827,6 +827,72 @@
} }
} }
}, },
"/notice/": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "Modifies the content of notice.txt, shown as notice box on the homepage.\nIf more than one formValue is set then only the highest priority field is used.\nOnly accessible from IPs registered with apiAllowedIPs configuration option.",
"consumes": [
"multipart/form-data"
],
"produces": [
"text/plain"
],
"tags": [
"User"
],
"summary": "Updates or empties the notice box content",
"parameters": [
{
"type": "string",
"description": "Priority 1: New content to display",
"name": "new-content",
"in": "formData"
}
],
"responses": {
"200": {
"description": "Success Response Message",
"schema": {
"type": "string"
}
},
"400": {
"description": "Bad Request",
"schema": {
"type": "string"
}
},
"401": {
"description": "Unauthorized",
"schema": {
"type": "string"
}
},
"403": {
"description": "Forbidden",
"schema": {
"type": "string"
}
},
"422": {
"description": "Unprocessable Entity: The user could not be updated",
"schema": {
"type": "string"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"type": "string"
}
}
}
}
},
"/user/{id}": { "/user/{id}": {
"post": { "post": {
"security": [ "security": [
@ -1207,6 +1273,14 @@
} }
} }
}, },
"api.DefaultJobApiResponse": {
"type": "object",
"properties": {
"msg": {
"type": "string"
}
}
},
"api.DeleteJobApiRequest": { "api.DeleteJobApiRequest": {
"type": "object", "type": "object",
"required": [ "required": [
@ -1230,14 +1304,6 @@
} }
} }
}, },
"api.DeleteJobApiResponse": {
"type": "object",
"properties": {
"msg": {
"type": "string"
}
}
},
"api.EditMetaRequest": { "api.EditMetaRequest": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -1324,14 +1390,6 @@
} }
} }
}, },
"api.StartJobApiResponse": {
"type": "object",
"properties": {
"msg": {
"type": "string"
}
}
},
"api.StopJobApiRequest": { "api.StopJobApiRequest": {
"type": "object", "type": "object",
"required": [ "required": [

@ -32,6 +32,11 @@ definitions:
example: Debug example: Debug
type: string type: string
type: object type: object
api.DefaultJobApiResponse:
properties:
msg:
type: string
type: object
api.DeleteJobApiRequest: api.DeleteJobApiRequest:
properties: properties:
cluster: cluster:
@ -49,11 +54,6 @@ definitions:
required: required:
- jobId - jobId
type: object type: object
api.DeleteJobApiResponse:
properties:
msg:
type: string
type: object
api.EditMetaRequest: api.EditMetaRequest:
properties: properties:
key: key:
@ -112,11 +112,6 @@ definitions:
scope: scope:
$ref: '#/definitions/schema.MetricScope' $ref: '#/definitions/schema.MetricScope'
type: object type: object
api.StartJobApiResponse:
properties:
msg:
type: string
type: object
api.StopJobApiRequest: api.StopJobApiRequest:
properties: properties:
cluster: cluster:
@ -906,7 +901,7 @@ paths:
"200": "200":
description: Success message description: Success message
schema: schema:
$ref: '#/definitions/api.DeleteJobApiResponse' $ref: '#/definitions/api.DefaultJobApiResponse'
"400": "400":
description: Bad Request description: Bad Request
schema: schema:
@ -953,7 +948,7 @@ paths:
"200": "200":
description: Success message description: Success message
schema: schema:
$ref: '#/definitions/api.DeleteJobApiResponse' $ref: '#/definitions/api.DefaultJobApiResponse'
"400": "400":
description: Bad Request description: Bad Request
schema: schema:
@ -1000,7 +995,7 @@ paths:
"200": "200":
description: Success message description: Success message
schema: schema:
$ref: '#/definitions/api.DeleteJobApiResponse' $ref: '#/definitions/api.DefaultJobApiResponse'
"400": "400":
description: Bad Request description: Bad Request
schema: schema:
@ -1098,7 +1093,7 @@ paths:
"201": "201":
description: Job added successfully description: Job added successfully
schema: schema:
$ref: '#/definitions/api.StartJobApiResponse' $ref: '#/definitions/api.DefaultJobApiResponse'
"400": "400":
description: Bad Request description: Bad Request
schema: schema:
@ -1161,8 +1156,7 @@ paths:
schema: schema:
$ref: '#/definitions/api.ErrorResponse' $ref: '#/definitions/api.ErrorResponse'
"422": "422":
description: 'Unprocessable Entity: finding job failed: sql: no rows in description: 'Unprocessable Entity: job has already been stopped'
result set'
schema: schema:
$ref: '#/definitions/api.ErrorResponse' $ref: '#/definitions/api.ErrorResponse'
"500": "500":
@ -1224,6 +1218,51 @@ paths:
summary: Adds one or more tags to a job summary: Adds one or more tags to a job
tags: tags:
- Job add and modify - Job add and modify
/notice/:
post:
consumes:
- multipart/form-data
description: |-
Modifies the content of notice.txt, shown as notice box on the homepage.
If more than one formValue is set then only the highest priority field is used.
Only accessible from IPs registered with apiAllowedIPs configuration option.
parameters:
- description: 'Priority 1: New content to display'
in: formData
name: new-content
type: string
produces:
- text/plain
responses:
"200":
description: Success Response Message
schema:
type: string
"400":
description: Bad Request
schema:
type: string
"401":
description: Unauthorized
schema:
type: string
"403":
description: Forbidden
schema:
type: string
"422":
description: 'Unprocessable Entity: The user could not be updated'
schema:
type: string
"500":
description: Internal Server Error
schema:
type: string
security:
- ApiKeyAuth: []
summary: Updates or empties the notice box content
tags:
- User
/user/{id}: /user/{id}:
post: post:
consumes: consumes:

47
go.mod

@ -3,33 +3,33 @@ module github.com/ClusterCockpit/cc-backend
go 1.23.5 go 1.23.5
require ( require (
github.com/99designs/gqlgen v0.17.63 github.com/99designs/gqlgen v0.17.66
github.com/ClusterCockpit/cc-units v0.4.0 github.com/ClusterCockpit/cc-units v0.4.0
github.com/Masterminds/squirrel v1.5.4 github.com/Masterminds/squirrel v1.5.4
github.com/coreos/go-oidc/v3 v3.11.0 github.com/coreos/go-oidc/v3 v3.12.0
github.com/go-co-op/gocron/v2 v2.9.0 github.com/go-co-op/gocron/v2 v2.16.0
github.com/go-ldap/ldap/v3 v3.4.8 github.com/go-ldap/ldap/v3 v3.4.10
github.com/go-sql-driver/mysql v1.8.1 github.com/go-sql-driver/mysql v1.9.0
github.com/golang-jwt/jwt/v5 v5.2.1 github.com/golang-jwt/jwt/v5 v5.2.1
github.com/golang-migrate/migrate/v4 v4.17.1 github.com/golang-migrate/migrate/v4 v4.18.2
github.com/google/gops v0.3.28 github.com/google/gops v0.3.28
github.com/gorilla/handlers v1.5.2 github.com/gorilla/handlers v1.5.2
github.com/gorilla/mux v1.8.1 github.com/gorilla/mux v1.8.1
github.com/gorilla/sessions v1.4.0 github.com/gorilla/sessions v1.4.0
github.com/influxdata/influxdb-client-go/v2 v2.13.0 github.com/influxdata/influxdb-client-go/v2 v2.14.0
github.com/jmoiron/sqlx v1.4.0 github.com/jmoiron/sqlx v1.4.0
github.com/mattn/go-sqlite3 v1.14.22 github.com/mattn/go-sqlite3 v1.14.24
github.com/prometheus/client_golang v1.19.1 github.com/prometheus/client_golang v1.21.0
github.com/prometheus/common v0.55.0 github.com/prometheus/common v0.62.0
github.com/qustavo/sqlhooks/v2 v2.1.0 github.com/qustavo/sqlhooks/v2 v2.1.0
github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 github.com/santhosh-tekuri/jsonschema/v5 v5.3.1
github.com/swaggo/http-swagger v1.3.4 github.com/swaggo/http-swagger v1.3.4
github.com/swaggo/swag v1.16.4 github.com/swaggo/swag v1.16.4
github.com/vektah/gqlparser/v2 v2.5.22 github.com/vektah/gqlparser/v2 v2.5.22
golang.org/x/crypto v0.32.0 golang.org/x/crypto v0.35.0
golang.org/x/exp v0.0.0-20240707233637-46b078467d37 golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa
golang.org/x/oauth2 v0.21.0 golang.org/x/oauth2 v0.27.0
golang.org/x/time v0.10.0 golang.org/x/time v0.5.0
) )
require ( require (
@ -43,7 +43,7 @@ require (
github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-asn1-ber/asn1-ber v1.5.7 // indirect github.com/go-asn1-ber/asn1-ber v1.5.7 // indirect
github.com/go-jose/go-jose/v4 v4.0.3 // indirect github.com/go-jose/go-jose/v4 v4.0.5 // indirect
github.com/go-openapi/jsonpointer v0.21.0 // indirect github.com/go-openapi/jsonpointer v0.21.0 // indirect
github.com/go-openapi/jsonreference v0.21.0 // indirect github.com/go-openapi/jsonreference v0.21.0 // indirect
github.com/go-openapi/spec v0.21.0 // indirect github.com/go-openapi/spec v0.21.0 // indirect
@ -56,7 +56,7 @@ require (
github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/influxdata/line-protocol v0.0.0-20210922203350-b1ad95c89adf // indirect github.com/influxdata/line-protocol v0.0.0-20210922203350-b1ad95c89adf // indirect
github.com/jonboulle/clockwork v0.4.0 // indirect github.com/jonboulle/clockwork v0.5.0 // indirect
github.com/josharian/intern v1.0.0 // indirect github.com/josharian/intern v1.0.0 // indirect
github.com/jpillora/backoff v1.0.0 // indirect github.com/jpillora/backoff v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect github.com/json-iterator/go v1.1.12 // indirect
@ -68,7 +68,6 @@ require (
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect
github.com/oapi-codegen/runtime v1.1.1 // indirect github.com/oapi-codegen/runtime v1.1.1 // indirect
github.com/opencontainers/image-spec v1.1.0-rc2.0.20221005185240-3a7f492d3f1b // indirect
github.com/prometheus/client_model v0.6.1 // indirect github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/procfs v0.15.1 // indirect github.com/prometheus/procfs v0.15.1 // indirect
github.com/robfig/cron/v3 v3.0.1 // indirect github.com/robfig/cron/v3 v3.0.1 // indirect
@ -78,13 +77,13 @@ require (
github.com/urfave/cli/v2 v2.27.5 // indirect github.com/urfave/cli/v2 v2.27.5 // indirect
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
go.uber.org/atomic v1.11.0 // indirect go.uber.org/atomic v1.11.0 // indirect
golang.org/x/mod v0.22.0 // indirect golang.org/x/mod v0.23.0 // indirect
golang.org/x/net v0.34.0 // indirect golang.org/x/net v0.35.0 // indirect
golang.org/x/sync v0.10.0 // indirect golang.org/x/sync v0.11.0 // indirect
golang.org/x/sys v0.29.0 // indirect golang.org/x/sys v0.30.0 // indirect
golang.org/x/text v0.21.0 // indirect golang.org/x/text v0.22.0 // indirect
golang.org/x/tools v0.29.0 // indirect golang.org/x/tools v0.30.0 // indirect
google.golang.org/protobuf v1.36.1 // indirect google.golang.org/protobuf v1.36.5 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
sigs.k8s.io/yaml v1.4.0 // indirect sigs.k8s.io/yaml v1.4.0 // indirect

160
go.sum

@ -1,7 +1,7 @@
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/99designs/gqlgen v0.17.63 h1:HCdaYDPd9HqUXRchEvmE3EFzELRwLlaJ8DBuyC8Cqto= github.com/99designs/gqlgen v0.17.66 h1:2/SRc+h3115fCOZeTtsqrB5R5gTGm+8qCAwcrZa+CXA=
github.com/99designs/gqlgen v0.17.63/go.mod h1:sVCM2iwIZisJjTI/DEC3fpH+HFgxY1496ZJ+jbT9IjA= github.com/99designs/gqlgen v0.17.66/go.mod h1:gucrb5jK5pgCKzAGuOMMVU9C8PnReecHEHd2UxLQwCg=
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0=
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8= github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8=
@ -12,8 +12,8 @@ github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM= github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM=
github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10= github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10=
github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/PuerkitoBio/goquery v1.9.3 h1:mpJr/ikUA9/GNJB/DBZcGeFDXUtosHRyRrwh7KGdTG0= github.com/PuerkitoBio/goquery v1.9.3 h1:mpJr/ikUA9/GNJB/DBZcGeFDXUtosHRyRrwh7KGdTG0=
github.com/PuerkitoBio/goquery v1.9.3/go.mod h1:1ndLHPdTz+DyQPICCWYlYQMPl0oXZj0G6D4LCYA6u4U= github.com/PuerkitoBio/goquery v1.9.3/go.mod h1:1ndLHPdTz+DyQPICCWYlYQMPl0oXZj0G6D4LCYA6u4U=
github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk= github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk=
@ -34,8 +34,8 @@ github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6r
github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/coreos/go-oidc/v3 v3.11.0 h1:Ia3MxdwpSw702YW0xgfmP1GVCMA9aEFWu12XUZ3/OtI= github.com/coreos/go-oidc/v3 v3.12.0 h1:sJk+8G2qq94rDI6ehZ71Bol3oUHy63qNYmkiSjrc/Jo=
github.com/coreos/go-oidc/v3 v3.11.0/go.mod h1:gE3LgjOgFoHi9a4ce4/tJczr0Ai2/BoDhf0r5lltWI0= github.com/coreos/go-oidc/v3 v3.12.0/go.mod h1:gE3LgjOgFoHi9a4ce4/tJczr0Ai2/BoDhf0r5lltWI0=
github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0= github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@ -43,27 +43,30 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54 h1:SG7nF6SRlWhcT7cNTs5R6Hk4V2lcmLz2NsG2VnInyNo= github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54 h1:SG7nF6SRlWhcT7cNTs5R6Hk4V2lcmLz2NsG2VnInyNo=
github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA= github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA=
github.com/dhui/dktest v0.4.1 h1:/w+IWuDXVymg3IrRJCHHOkMK10m9aNVMOyD0X12YVTg= github.com/dhui/dktest v0.4.4 h1:+I4s6JRE1yGuqflzwqG+aIaMdgXIorCf5P98JnaAWa8=
github.com/dhui/dktest v0.4.1/go.mod h1:DdOqcUpL7vgyP4GlF3X3w7HbSlz8cEQzwewPveYEQbA= github.com/dhui/dktest v0.4.4/go.mod h1:4+22R4lgsdAXrDyaH4Nqx2JEz2hLp49MqQmm9HLCQhM=
github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/docker/docker v24.0.9+incompatible h1:HPGzNmwfLZWdxHqK9/II92pyi1EpYKsAqcl4G0Of9v0= github.com/docker/docker v27.2.0+incompatible h1:Rk9nIVdfH3+Vz4cyI/uhbINhEZ/oLmc+CBXmH6fbNk4=
github.com/docker/docker v24.0.9+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker v27.2.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/go-asn1-ber/asn1-ber v1.5.5/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
github.com/go-asn1-ber/asn1-ber v1.5.7 h1:DTX+lbVTWaTw1hQ+PbZPlnDZPEIs0SS/GCZAl535dDk= github.com/go-asn1-ber/asn1-ber v1.5.7 h1:DTX+lbVTWaTw1hQ+PbZPlnDZPEIs0SS/GCZAl535dDk=
github.com/go-asn1-ber/asn1-ber v1.5.7/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= github.com/go-asn1-ber/asn1-ber v1.5.7/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
github.com/go-co-op/gocron/v2 v2.9.0 h1:+0nTyI3mjc2FGIClBdDWpaLPCNrJ+62o9xbS0ZklEKQ= github.com/go-co-op/gocron/v2 v2.16.0 h1:uqUF6WFZ4enRU45pWFNcn1xpDLc+jBOTKhPQI16Z1xs=
github.com/go-co-op/gocron/v2 v2.9.0/go.mod h1:xY7bJxGazKam1cz04EebrlP4S9q4iWdiAylMGP3jY9w= github.com/go-co-op/gocron/v2 v2.16.0/go.mod h1:opexeOFy5BplhsKdA7bzY9zeYih8I8/WNJ4arTIFPVc=
github.com/go-jose/go-jose/v4 v4.0.3 h1:o8aphO8Hv6RPmH+GfzVuyf7YXSBibp+8YyHdOoDESGo= github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE=
github.com/go-jose/go-jose/v4 v4.0.3/go.mod h1:NKb5HO1EZccyMpiZNbdUw/14tiXNyUJh188dfnMCAfc= github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA=
github.com/go-ldap/ldap/v3 v3.4.8 h1:loKJyspcRezt2Q3ZRMq2p/0v8iOurlmeXDPw6fikSvQ= github.com/go-ldap/ldap/v3 v3.4.10 h1:ot/iwPOhfpNVgB1o+AVXljizWZ9JTp7YF5oeyONmcJU=
github.com/go-ldap/ldap/v3 v3.4.8/go.mod h1:qS3Sjlu76eHfHGpUdWkAXQTw4beih+cHsco2jXlIXrk= github.com/go-ldap/ldap/v3 v3.4.10/go.mod h1:JXh4Uxgi40P6E9rdsYqpUtbW46D9UTjJ9QSwGRznplY=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ=
@ -73,16 +76,17 @@ github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/go-sql-driver/mysql v1.9.0 h1:Y0zIbQXhQKmQgTp44Y1dp3wTXcn804QoTptLZT1vtvo=
github.com/go-sql-driver/mysql v1.9.0/go.mod h1:pDetrLJeA3oMujJuvXc8RJoasr589B6A9fwzD3QMrqw=
github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang-migrate/migrate/v4 v4.17.1 h1:4zQ6iqL6t6AiItphxJctQb3cFqWiSpMnX7wLTPnnYO4= github.com/golang-migrate/migrate/v4 v4.18.2 h1:2VSCMz7x7mjyTXx3m2zPokOY82LTRgxK1yQYKo6wWQ8=
github.com/golang-migrate/migrate/v4 v4.17.1/go.mod h1:m8hinFyWBn0SA4QKHuKh175Pm9wjmxj3S2Mia7dbXzM= github.com/golang-migrate/migrate/v4 v4.18.2/go.mod h1:2CM6tJvn2kqPXwnXO/d3rAQYiyoIm180VsO8PRX6Rpk=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
@ -115,8 +119,8 @@ github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/C
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/influxdata/influxdb-client-go/v2 v2.13.0 h1:ioBbLmR5NMbAjP4UVA5r9b5xGjpABD7j65pI8kFphDM= github.com/influxdata/influxdb-client-go/v2 v2.14.0 h1:AjbBfJuq+QoaXNcrova8smSjwJdUHnwvfjMF71M1iI4=
github.com/influxdata/influxdb-client-go/v2 v2.13.0/go.mod h1:k+spCbt9hcvqvUiz0sr5D8LolXHqAAOfPw9v/RIRHl4= github.com/influxdata/influxdb-client-go/v2 v2.14.0/go.mod h1:Ahpm3QXKMJslpXl3IftVLVezreAUtBOTZssDrjZEFHI=
github.com/influxdata/line-protocol v0.0.0-20210922203350-b1ad95c89adf h1:7JTmneyiNEwVBOHSjoMxiWAqB992atOeepeFYegn5RU= github.com/influxdata/line-protocol v0.0.0-20210922203350-b1ad95c89adf h1:7JTmneyiNEwVBOHSjoMxiWAqB992atOeepeFYegn5RU=
github.com/influxdata/line-protocol v0.0.0-20210922203350-b1ad95c89adf/go.mod h1:xaLFMmpvUxqXtVkUJfg9QmT88cDaCJ3ZKgdZ78oO8Qo= github.com/influxdata/line-protocol v0.0.0-20210922203350-b1ad95c89adf/go.mod h1:xaLFMmpvUxqXtVkUJfg9QmT88cDaCJ3ZKgdZ78oO8Qo=
github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8= github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8=
@ -133,8 +137,8 @@ github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZ
github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=
github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
github.com/jonboulle/clockwork v0.4.0 h1:p4Cf1aMWXnXAUh8lVfewRBx1zaTSYKrKMF2g3ST4RZ4= github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I=
github.com/jonboulle/clockwork v0.4.0/go.mod h1:xgRqUGwRcjKCO1vbZUEtSLrqKoPSsUpK7fnezOII0kc= github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA= github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA=
@ -142,6 +146,8 @@ github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE= github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE=
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
@ -156,8 +162,11 @@ github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@ -175,27 +184,27 @@ github.com/oapi-codegen/runtime v1.1.1 h1:EXLHh0DXIJnWhdRPN2w4MXAzFyE4CskzhNLUmt
github.com/oapi-codegen/runtime v1.1.1/go.mod h1:SK9X900oXmPWilYR5/WKPzt3Kqxn/uS/+lbpREv+eCg= github.com/oapi-codegen/runtime v1.1.1/go.mod h1:SK9X900oXmPWilYR5/WKPzt3Kqxn/uS/+lbpREv+eCg=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.0-rc2.0.20221005185240-3a7f492d3f1b h1:YWuSjZCQAPM8UUBLkYUk1e+rZcvWHJmFb6i6rM44Xs8= github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
github.com/opencontainers/image-spec v1.1.0-rc2.0.20221005185240-3a7f492d3f1b/go.mod h1:3OVijpioIKYWTqjiG0zfF6wvoJ4fAXGbjdZuI2NgsRQ= github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE= github.com/prometheus/client_golang v1.21.0 h1:DIsaGmiaBkSangBgMtWdNfxbMNdku5IK6iNhrEqWvdA=
github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho= github.com/prometheus/client_golang v1.21.0/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg=
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc= github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io=
github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
github.com/qustavo/sqlhooks/v2 v2.1.0 h1:54yBemHnGHp/7xgT+pxwmIlMSDNYKx5JW5dfRAiCZi0= github.com/qustavo/sqlhooks/v2 v2.1.0 h1:54yBemHnGHp/7xgT+pxwmIlMSDNYKx5JW5dfRAiCZi0=
github.com/qustavo/sqlhooks/v2 v2.1.0/go.mod h1:aMREyKo7fOKTwiLuWPsaHRXEmtqG4yREztO0idF83AU= github.com/qustavo/sqlhooks/v2 v2.1.0/go.mod h1:aMREyKo7fOKTwiLuWPsaHRXEmtqG4yREztO0idF83AU=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 h1:lZUw3E0/J3roVtGQ+SCrUrg3ON6NgVqpn3+iol9aGu4= github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 h1:lZUw3E0/J3roVtGQ+SCrUrg3ON6NgVqpn3+iol9aGu4=
@ -229,6 +238,14 @@ github.com/vektah/gqlparser/v2 v2.5.22/go.mod h1:xMl+ta8a5M1Yo1A1Iwt/k7gSpscwSnH
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8=
go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw=
go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8=
go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2g+8YLc=
go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8=
go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4=
go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
@ -236,16 +253,21 @@ go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs=
golang.org/x/exp v0.0.0-20240707233637-46b078467d37 h1:uLDX+AfeFCct3a2C7uIWBKMJIR3CJMhcgfrUAqjRK6w= golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ=
golang.org/x/exp v0.0.0-20240707233637-46b078467d37/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa h1:t2QcU6V556bFjYgu4L6C+6VrCPyJZ+eyRsABUPs1mz4=
golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa/go.mod h1:BHOTPb3L19zxehTsLoJXVaTktb06DFgmdW6Wb9s8jqk=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM=
golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
@ -253,17 +275,23 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs= golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M=
golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@ -271,35 +299,45 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4= golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.29.0 h1:Xx0h3TtM9rzQpQuR4dKLrdglAmCEN5Oi+P74JdhdzXE= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.29.0/go.mod h1:KMQVMRsVxU6nHCFXrBPhDB8XncLNLM0lIy/F14RP588= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY=
golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk= google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=

@ -30,6 +30,7 @@ resolver:
# gqlgen will search for any type names in the schema in these go packages # gqlgen will search for any type names in the schema in these go packages
# if they match it will use them, otherwise it will generate them. # if they match it will use them, otherwise it will generate them.
autobind: autobind:
- "github.com/99designs/gqlgen/graphql/introspection"
- "github.com/ClusterCockpit/cc-backend/internal/graph/model" - "github.com/ClusterCockpit/cc-backend/internal/graph/model"
# This section declares type mapping between the GraphQL and go type systems # This section declares type mapping between the GraphQL and go type systems

@ -208,7 +208,7 @@ const docTemplate = `{
"200": { "200": {
"description": "Success message", "description": "Success message",
"schema": { "schema": {
"$ref": "#/definitions/api.DeleteJobApiResponse" "$ref": "#/definitions/api.DefaultJobApiResponse"
} }
}, },
"400": { "400": {
@ -278,7 +278,7 @@ const docTemplate = `{
"200": { "200": {
"description": "Success message", "description": "Success message",
"schema": { "schema": {
"$ref": "#/definitions/api.DeleteJobApiResponse" "$ref": "#/definitions/api.DefaultJobApiResponse"
} }
}, },
"400": { "400": {
@ -348,7 +348,7 @@ const docTemplate = `{
"200": { "200": {
"description": "Success message", "description": "Success message",
"schema": { "schema": {
"$ref": "#/definitions/api.DeleteJobApiResponse" "$ref": "#/definitions/api.DefaultJobApiResponse"
} }
}, },
"400": { "400": {
@ -493,7 +493,7 @@ const docTemplate = `{
"201": { "201": {
"description": "Job added successfully", "description": "Job added successfully",
"schema": { "schema": {
"$ref": "#/definitions/api.StartJobApiResponse" "$ref": "#/definitions/api.DefaultJobApiResponse"
} }
}, },
"400": { "400": {
@ -587,7 +587,7 @@ const docTemplate = `{
} }
}, },
"422": { "422": {
"description": "Unprocessable Entity: finding job failed: sql: no rows in result set", "description": "Unprocessable Entity: job has already been stopped",
"schema": { "schema": {
"$ref": "#/definitions/api.ErrorResponse" "$ref": "#/definitions/api.ErrorResponse"
} }
@ -833,6 +833,72 @@ const docTemplate = `{
} }
} }
}, },
"/notice/": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "Modifies the content of notice.txt, shown as notice box on the homepage.\nIf more than one formValue is set then only the highest priority field is used.\nOnly accessible from IPs registered with apiAllowedIPs configuration option.",
"consumes": [
"multipart/form-data"
],
"produces": [
"text/plain"
],
"tags": [
"User"
],
"summary": "Updates or empties the notice box content",
"parameters": [
{
"type": "string",
"description": "Priority 1: New content to display",
"name": "new-content",
"in": "formData"
}
],
"responses": {
"200": {
"description": "Success Response Message",
"schema": {
"type": "string"
}
},
"400": {
"description": "Bad Request",
"schema": {
"type": "string"
}
},
"401": {
"description": "Unauthorized",
"schema": {
"type": "string"
}
},
"403": {
"description": "Forbidden",
"schema": {
"type": "string"
}
},
"422": {
"description": "Unprocessable Entity: The user could not be updated",
"schema": {
"type": "string"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"type": "string"
}
}
}
}
},
"/user/{id}": { "/user/{id}": {
"post": { "post": {
"security": [ "security": [
@ -1213,6 +1279,14 @@ const docTemplate = `{
} }
} }
}, },
"api.DefaultJobApiResponse": {
"type": "object",
"properties": {
"msg": {
"type": "string"
}
}
},
"api.DeleteJobApiRequest": { "api.DeleteJobApiRequest": {
"type": "object", "type": "object",
"required": [ "required": [
@ -1236,14 +1310,6 @@ const docTemplate = `{
} }
} }
}, },
"api.DeleteJobApiResponse": {
"type": "object",
"properties": {
"msg": {
"type": "string"
}
}
},
"api.EditMetaRequest": { "api.EditMetaRequest": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -1330,14 +1396,6 @@ const docTemplate = `{
} }
} }
}, },
"api.StartJobApiResponse": {
"type": "object",
"properties": {
"msg": {
"type": "string"
}
}
},
"api.StopJobApiRequest": { "api.StopJobApiRequest": {
"type": "object", "type": "object",
"required": [ "required": [

@ -757,7 +757,7 @@ func (api *RestApi) tagJob(rw http.ResponseWriter, r *http.Request) {
// @accept json // @accept json
// @produce json // @produce json
// @param request body schema.JobMeta true "Job to add" // @param request body schema.JobMeta true "Job to add"
// @success 201 {object} api.StartJobApiResponse "Job added successfully" // @success 201 {object} api.DefaultJobApiResponse "Job added successfully"
// @failure 400 {object} api.ErrorResponse "Bad Request" // @failure 400 {object} api.ErrorResponse "Bad Request"
// @failure 401 {object} api.ErrorResponse "Unauthorized" // @failure 401 {object} api.ErrorResponse "Unauthorized"
// @failure 403 {object} api.ErrorResponse "Forbidden" // @failure 403 {object} api.ErrorResponse "Forbidden"
@ -772,9 +772,8 @@ func (api *RestApi) startJob(rw http.ResponseWriter, r *http.Request) {
return return
} }
if req.State == "" {
req.State = schema.JobStateRunning req.State = schema.JobStateRunning
}
if err := importer.SanityChecks(&req.BaseJob); err != nil { if err := importer.SanityChecks(&req.BaseJob); err != nil {
handleError(err, http.StatusBadRequest, rw) handleError(err, http.StatusBadRequest, rw)
return return
@ -835,7 +834,7 @@ func (api *RestApi) startJob(rw http.ResponseWriter, r *http.Request) {
// @failure 401 {object} api.ErrorResponse "Unauthorized" // @failure 401 {object} api.ErrorResponse "Unauthorized"
// @failure 403 {object} api.ErrorResponse "Forbidden" // @failure 403 {object} api.ErrorResponse "Forbidden"
// @failure 404 {object} api.ErrorResponse "Resource not found" // @failure 404 {object} api.ErrorResponse "Resource not found"
// @failure 422 {object} api.ErrorResponse "Unprocessable Entity: finding job failed: sql: no rows in result set" // @failure 422 {object} api.ErrorResponse "Unprocessable Entity: job has already been stopped"
// @failure 500 {object} api.ErrorResponse "Internal Server Error" // @failure 500 {object} api.ErrorResponse "Internal Server Error"
// @security ApiKeyAuth // @security ApiKeyAuth
// @router /jobs/stop_job/ [post] // @router /jobs/stop_job/ [post]
@ -871,7 +870,7 @@ func (api *RestApi) stopJobByRequest(rw http.ResponseWriter, r *http.Request) {
// @description Job to remove is specified by database ID. This will not remove the job from the job archive. // @description Job to remove is specified by database ID. This will not remove the job from the job archive.
// @produce json // @produce json
// @param id path int true "Database ID of Job" // @param id path int true "Database ID of Job"
// @success 200 {object} api.DeleteJobApiResponse "Success message" // @success 200 {object} api.DefaultJobApiResponse "Success message"
// @failure 400 {object} api.ErrorResponse "Bad Request" // @failure 400 {object} api.ErrorResponse "Bad Request"
// @failure 401 {object} api.ErrorResponse "Unauthorized" // @failure 401 {object} api.ErrorResponse "Unauthorized"
// @failure 403 {object} api.ErrorResponse "Forbidden" // @failure 403 {object} api.ErrorResponse "Forbidden"
@ -914,7 +913,7 @@ func (api *RestApi) deleteJobById(rw http.ResponseWriter, r *http.Request) {
// @accept json // @accept json
// @produce json // @produce json
// @param request body api.DeleteJobApiRequest true "All fields required" // @param request body api.DeleteJobApiRequest true "All fields required"
// @success 200 {object} api.DeleteJobApiResponse "Success message" // @success 200 {object} api.DefaultJobApiResponse "Success message"
// @failure 400 {object} api.ErrorResponse "Bad Request" // @failure 400 {object} api.ErrorResponse "Bad Request"
// @failure 401 {object} api.ErrorResponse "Unauthorized" // @failure 401 {object} api.ErrorResponse "Unauthorized"
// @failure 403 {object} api.ErrorResponse "Forbidden" // @failure 403 {object} api.ErrorResponse "Forbidden"
@ -964,7 +963,7 @@ func (api *RestApi) deleteJobByRequest(rw http.ResponseWriter, r *http.Request)
// @description Remove all jobs with start time before timestamp. The jobs will not be removed from the job archive. // @description Remove all jobs with start time before timestamp. The jobs will not be removed from the job archive.
// @produce json // @produce json
// @param ts path int true "Unix epoch timestamp" // @param ts path int true "Unix epoch timestamp"
// @success 200 {object} api.DeleteJobApiResponse "Success message" // @success 200 {object} api.DefaultJobApiResponse "Success message"
// @failure 400 {object} api.ErrorResponse "Bad Request" // @failure 400 {object} api.ErrorResponse "Bad Request"
// @failure 401 {object} api.ErrorResponse "Unauthorized" // @failure 401 {object} api.ErrorResponse "Unauthorized"
// @failure 403 {object} api.ErrorResponse "Forbidden" // @failure 403 {object} api.ErrorResponse "Forbidden"
@ -1004,8 +1003,13 @@ func (api *RestApi) deleteJobBefore(rw http.ResponseWriter, r *http.Request) {
func (api *RestApi) checkAndHandleStopJob(rw http.ResponseWriter, job *schema.Job, req StopJobApiRequest) { func (api *RestApi) checkAndHandleStopJob(rw http.ResponseWriter, job *schema.Job, req StopJobApiRequest) {
// Sanity checks // Sanity checks
if job == nil || job.StartTime.Unix() >= req.StopTime || job.State != schema.JobStateRunning { if job.State != schema.JobStateRunning {
handleError(fmt.Errorf("jobId %d (id %d) on %s : stopTime %d must be larger than startTime %d and only running jobs can be stopped (state is: %s)", job.JobID, job.ID, job.Cluster, req.StopTime, job.StartTime.Unix(), job.State), http.StatusBadRequest, rw) handleError(fmt.Errorf("jobId %d (id %d) on %s : job has already been stopped (state is: %s)", job.JobID, job.ID, job.Cluster, job.State), http.StatusUnprocessableEntity, rw)
return
}
if job == nil || job.StartTime.Unix() > req.StopTime {
handleError(fmt.Errorf("jobId %d (id %d) on %s : stopTime %d must be larger/equal than startTime %d", job.JobID, job.ID, job.Cluster, req.StopTime, job.StartTime.Unix()), http.StatusBadRequest, rw)
return return
} }

@ -16,7 +16,7 @@ type DefaultMetricsConfig struct {
} }
func LoadDefaultMetricsConfig() (*DefaultMetricsConfig, error) { func LoadDefaultMetricsConfig() (*DefaultMetricsConfig, error) {
filePath := "configs/default_metrics.json" filePath := "default_metrics.json"
if _, err := os.Stat(filePath); os.IsNotExist(err) { if _, err := os.Stat(filePath); os.IsNotExist(err) {
return nil, nil return nil, nil
} }

File diff suppressed because it is too large Load Diff

@ -81,6 +81,11 @@ type JobLinkResultList struct {
Count *int `json:"count,omitempty"` Count *int `json:"count,omitempty"`
} }
type JobMetricStatWithName struct {
Name string `json:"name"`
Stats *schema.MetricStatistics `json:"stats"`
}
type JobMetricWithName struct { type JobMetricWithName struct {
Name string `json:"name"` Name string `json:"name"`
Scope schema.MetricScope `json:"scope"` Scope schema.MetricScope `json:"scope"`
@ -168,9 +173,6 @@ type PageRequest struct {
Page int `json:"page"` Page int `json:"page"`
} }
type Query struct {
}
type StringInput struct { type StringInput struct {
Eq *string `json:"eq,omitempty"` Eq *string `json:"eq,omitempty"`
Neq *string `json:"neq,omitempty"` Neq *string `json:"neq,omitempty"`
@ -224,7 +226,7 @@ func (e Aggregate) String() string {
return string(e) return string(e)
} }
func (e *Aggregate) UnmarshalGQL(v interface{}) error { func (e *Aggregate) UnmarshalGQL(v any) error {
str, ok := v.(string) str, ok := v.(string)
if !ok { if !ok {
return fmt.Errorf("enums must be strings") return fmt.Errorf("enums must be strings")
@ -277,7 +279,7 @@ func (e SortByAggregate) String() string {
return string(e) return string(e)
} }
func (e *SortByAggregate) UnmarshalGQL(v interface{}) error { func (e *SortByAggregate) UnmarshalGQL(v any) error {
str, ok := v.(string) str, ok := v.(string)
if !ok { if !ok {
return fmt.Errorf("enums must be strings") return fmt.Errorf("enums must be strings")
@ -318,7 +320,7 @@ func (e SortDirectionEnum) String() string {
return string(e) return string(e)
} }
func (e *SortDirectionEnum) UnmarshalGQL(v interface{}) error { func (e *SortDirectionEnum) UnmarshalGQL(v any) error {
str, ok := v.(string) str, ok := v.(string)
if !ok { if !ok {
return fmt.Errorf("enums must be strings") return fmt.Errorf("enums must be strings")

@ -2,7 +2,7 @@ package graph
// This file will be automatically regenerated based on the schema, any resolver implementations // This file will be automatically regenerated based on the schema, any resolver implementations
// will be copied through when generating and any unknown code will be moved to the end. // will be copied through when generating and any unknown code will be moved to the end.
// Code generated by github.com/99designs/gqlgen version v0.17.57 // Code generated by github.com/99designs/gqlgen version v0.17.66
import ( import (
"context" "context"
@ -301,6 +301,32 @@ func (r *queryResolver) JobMetrics(ctx context.Context, id string, metrics []str
return res, err return res, err
} }
// JobMetricStats is the resolver for the jobMetricStats field.
func (r *queryResolver) JobMetricStats(ctx context.Context, id string, metrics []string) ([]*model.JobMetricStatWithName, error) {
job, err := r.Query().Job(ctx, id)
if err != nil {
log.Warn("Error while querying job for metrics")
return nil, err
}
data, err := metricDataDispatcher.LoadStatData(job, metrics, ctx)
if err != nil {
log.Warn("Error while loading job stat data")
return nil, err
}
res := []*model.JobMetricStatWithName{}
for name, md := range data {
res = append(res, &model.JobMetricStatWithName{
Name: name,
Stats: &md,
})
}
return res, err
}
// JobsFootprints is the resolver for the jobsFootprints field. // JobsFootprints is the resolver for the jobsFootprints field.
func (r *queryResolver) JobsFootprints(ctx context.Context, filter []*model.JobFilter, metrics []string) (*model.Footprints, error) { func (r *queryResolver) JobsFootprints(ctx context.Context, filter []*model.JobFilter, metrics []string) (*model.Footprints, error) {
// NOTE: Legacy Naming! This resolver is for normalized histograms in analysis view only - *Not* related to DB "footprint" column! // NOTE: Legacy Naming! This resolver is for normalized histograms in analysis view only - *Not* related to DB "footprint" column!

@ -96,27 +96,35 @@ func HandleImportFlag(flag string) error {
} }
job.EnergyFootprint = make(map[string]float64) job.EnergyFootprint = make(map[string]float64)
var totalEnergy float64
var energy float64
// Total Job Energy Outside Loop
totalEnergy := 0.0
for _, fp := range sc.EnergyFootprint { for _, fp := range sc.EnergyFootprint {
// Always Init Metric Energy Inside Loop
metricEnergy := 0.0
if i, err := archive.MetricIndex(sc.MetricConfig, fp); err == nil { if i, err := archive.MetricIndex(sc.MetricConfig, fp); err == nil {
// Note: For DB data, calculate and save as kWh // Note: For DB data, calculate and save as kWh
// Energy: Power (in Watts) * Time (in Seconds)
if sc.MetricConfig[i].Energy == "energy" { // this metric has energy as unit (Joules) if sc.MetricConfig[i].Energy == "energy" { // this metric has energy as unit (Joules)
log.Warnf("Update EnergyFootprint for Job %d and Metric %s on cluster %s: Set to 'energy' in cluster.json: Not implemented, will return 0.0", job.JobID, job.Cluster, fp)
// FIXME: Needs sum as stats type
} else if sc.MetricConfig[i].Energy == "power" { // this metric has power as unit (Watt) } else if sc.MetricConfig[i].Energy == "power" { // this metric has power as unit (Watt)
// Unit: ( W * s ) / 3600 / 1000 = kWh ; Rounded to 2 nearest digits // Energy: Power (in Watts) * Time (in Seconds)
energy = math.Round(((repository.LoadJobStat(&job, fp, "avg")*float64(job.Duration))/3600/1000)*100) / 100 // Unit: (W * (s / 3600)) / 1000 = kWh
// Round 2 Digits: round(Energy * 100) / 100
// Here: (All-Node Metric Average * Number of Nodes) * (Job Duration in Seconds / 3600) / 1000
// Note: Shared Jobs handled correctly since "Node Average" is based on partial resources, while "numNodes" factor is 1
rawEnergy := ((repository.LoadJobStat(&job, fp, "avg") * float64(job.NumNodes)) * (float64(job.Duration) / 3600.0)) / 1000.0
metricEnergy = math.Round(rawEnergy*100.0) / 100.0
} }
} else { } else {
log.Warnf("Error while collecting energy metric %s for job, DB ID '%v', return '0.0'", fp, job.ID) log.Warnf("Error while collecting energy metric %s for job, DB ID '%v', return '0.0'", fp, job.ID)
} }
job.EnergyFootprint[fp] = energy job.EnergyFootprint[fp] = metricEnergy
totalEnergy += energy totalEnergy += metricEnergy
} }
job.Energy = (math.Round(totalEnergy*100) / 100) job.Energy = (math.Round(totalEnergy*100.0) / 100.0)
if job.RawEnergyFootprint, err = json.Marshal(job.EnergyFootprint); err != nil { if job.RawEnergyFootprint, err = json.Marshal(job.EnergyFootprint); err != nil {
log.Warnf("Error while marshaling energy footprint for job INTO BYTES, DB ID '%v'", job.ID) log.Warnf("Error while marshaling energy footprint for job INTO BYTES, DB ID '%v'", job.ID)
return err return err

@ -93,27 +93,35 @@ func InitDB() error {
} }
job.EnergyFootprint = make(map[string]float64) job.EnergyFootprint = make(map[string]float64)
var totalEnergy float64
var energy float64
// Total Job Energy Outside Loop
totalEnergy := 0.0
for _, fp := range sc.EnergyFootprint { for _, fp := range sc.EnergyFootprint {
// Always Init Metric Energy Inside Loop
metricEnergy := 0.0
if i, err := archive.MetricIndex(sc.MetricConfig, fp); err == nil { if i, err := archive.MetricIndex(sc.MetricConfig, fp); err == nil {
// Note: For DB data, calculate and save as kWh // Note: For DB data, calculate and save as kWh
// Energy: Power (in Watts) * Time (in Seconds)
if sc.MetricConfig[i].Energy == "energy" { // this metric has energy as unit (Joules) if sc.MetricConfig[i].Energy == "energy" { // this metric has energy as unit (Joules)
log.Warnf("Update EnergyFootprint for Job %d and Metric %s on cluster %s: Set to 'energy' in cluster.json: Not implemented, will return 0.0", jobMeta.JobID, jobMeta.Cluster, fp)
// FIXME: Needs sum as stats type
} else if sc.MetricConfig[i].Energy == "power" { // this metric has power as unit (Watt) } else if sc.MetricConfig[i].Energy == "power" { // this metric has power as unit (Watt)
// Unit: ( W * s ) / 3600 / 1000 = kWh ; Rounded to 2 nearest digits // Energy: Power (in Watts) * Time (in Seconds)
energy = math.Round(((repository.LoadJobStat(jobMeta, fp, "avg")*float64(jobMeta.Duration))/3600/1000)*100) / 100 // Unit: (W * (s / 3600)) / 1000 = kWh
// Round 2 Digits: round(Energy * 100) / 100
// Here: (All-Node Metric Average * Number of Nodes) * (Job Duration in Seconds / 3600) / 1000
// Note: Shared Jobs handled correctly since "Node Average" is based on partial resources, while "numNodes" factor is 1
rawEnergy := ((repository.LoadJobStat(jobMeta, fp, "avg") * float64(jobMeta.NumNodes)) * (float64(jobMeta.Duration) / 3600.0)) / 1000.0
metricEnergy = math.Round(rawEnergy*100.0) / 100.0
} }
} else { } else {
log.Warnf("Error while collecting energy metric %s for job, DB ID '%v', return '0.0'", fp, jobMeta.ID) log.Warnf("Error while collecting energy metric %s for job, DB ID '%v', return '0.0'", fp, jobMeta.ID)
} }
job.EnergyFootprint[fp] = energy job.EnergyFootprint[fp] = metricEnergy
totalEnergy += energy totalEnergy += metricEnergy
} }
job.Energy = (math.Round(totalEnergy*100) / 100) job.Energy = (math.Round(totalEnergy*100.0) / 100.0)
if job.RawEnergyFootprint, err = json.Marshal(job.EnergyFootprint); err != nil { if job.RawEnergyFootprint, err = json.Marshal(job.EnergyFootprint); err != nil {
log.Warnf("Error while marshaling energy footprint for job INTO BYTES, DB ID '%v'", jobMeta.ID) log.Warnf("Error while marshaling energy footprint for job INTO BYTES, DB ID '%v'", jobMeta.ID)
return err return err

@ -7,6 +7,7 @@ package metricDataDispatcher
import ( import (
"context" "context"
"fmt" "fmt"
"math"
"time" "time"
"github.com/ClusterCockpit/cc-backend/internal/config" "github.com/ClusterCockpit/cc-backend/internal/config"
@ -223,6 +224,52 @@ func LoadAverages(
return nil return nil
} }
// Used for polar plots in frontend
func LoadStatData(
job *schema.Job,
metrics []string,
ctx context.Context,
) (map[string]schema.MetricStatistics, error) {
if job.State != schema.JobStateRunning && !config.Keys.DisableArchive {
return archive.LoadStatsFromArchive(job, metrics)
}
data := make(map[string]schema.MetricStatistics, len(metrics))
repo, err := metricdata.GetMetricDataRepo(job.Cluster)
if err != nil {
return data, fmt.Errorf("METRICDATA/METRICDATA > no metric data repository configured for '%s'", job.Cluster)
}
stats, err := repo.LoadStats(job, metrics, ctx)
if err != nil {
log.Errorf("Error while loading statistics for job %v (User %v, Project %v)", job.JobID, job.User, job.Project)
return data, err
}
for _, m := range metrics {
sum, avg, min, max := 0.0, 0.0, 0.0, 0.0
nodes, ok := stats[m]
if !ok {
data[m] = schema.MetricStatistics{Min: min, Avg: avg, Max: max}
continue
}
for _, node := range nodes {
sum += node.Avg
min = math.Min(min, node.Min)
max = math.Max(max, node.Max)
}
data[m] = schema.MetricStatistics{
Avg: (math.Round((sum/float64(job.NumNodes))*100) / 100),
Min: (math.Round(min*100) / 100),
Max: (math.Round(max*100) / 100),
}
}
return data, nil
}
// Used for the classic node/system view. Returns a map of nodes to a map of metrics. // Used for the classic node/system view. Returns a map of nodes to a map of metrics.
func LoadNodeData( func LoadNodeData(
cluster string, cluster string,

@ -590,28 +590,34 @@ func (r *JobRepository) UpdateEnergy(
return stmt, err return stmt, err
} }
energyFootprint := make(map[string]float64) energyFootprint := make(map[string]float64)
var totalEnergy float64
var energy float64
// Total Job Energy Outside Loop
totalEnergy := 0.0
for _, fp := range sc.EnergyFootprint { for _, fp := range sc.EnergyFootprint {
// Always Init Metric Energy Inside Loop
metricEnergy := 0.0
if i, err := archive.MetricIndex(sc.MetricConfig, fp); err == nil { if i, err := archive.MetricIndex(sc.MetricConfig, fp); err == nil {
// Note: For DB data, calculate and save as kWh // Note: For DB data, calculate and save as kWh
if sc.MetricConfig[i].Energy == "energy" { // this metric has energy as unit (Joules or Wh) if sc.MetricConfig[i].Energy == "energy" { // this metric has energy as unit (Joules or Wh)
log.Warnf("Update EnergyFootprint for Job %d and Metric %s on cluster %s: Set to 'energy' in cluster.json: Not implemented, will return 0.0", jobMeta.JobID, jobMeta.Cluster, fp)
// FIXME: Needs sum as stats type // FIXME: Needs sum as stats type
} else if sc.MetricConfig[i].Energy == "power" { // this metric has power as unit (Watt) } else if sc.MetricConfig[i].Energy == "power" { // this metric has power as unit (Watt)
// Energy: Power (in Watts) * Time (in Seconds) // Energy: Power (in Watts) * Time (in Seconds)
// Unit: (( W * s ) / 3600) / 1000 = kWh ; Rounded to 2 nearest digits: (Energy * 100) / 100 // Unit: (W * (s / 3600)) / 1000 = kWh
// Here: All-Node Metric Average * Number of Nodes * Job Runtime // Round 2 Digits: round(Energy * 100) / 100
// Here: (All-Node Metric Average * Number of Nodes) * (Job Duration in Seconds / 3600) / 1000
// Note: Shared Jobs handled correctly since "Node Average" is based on partial resources, while "numNodes" factor is 1 // Note: Shared Jobs handled correctly since "Node Average" is based on partial resources, while "numNodes" factor is 1
metricNodeSum := LoadJobStat(jobMeta, fp, "avg") * float64(jobMeta.NumNodes) * float64(jobMeta.Duration) rawEnergy := ((LoadJobStat(jobMeta, fp, "avg") * float64(jobMeta.NumNodes)) * (float64(jobMeta.Duration) / 3600.0)) / 1000.0
energy = math.Round(((metricNodeSum/3600)/1000)*100) / 100 metricEnergy = math.Round(rawEnergy*100.0) / 100.0
} }
} else { } else {
log.Warnf("Error while collecting energy metric %s for job, DB ID '%v', return '0.0'", fp, jobMeta.ID) log.Warnf("Error while collecting energy metric %s for job, DB ID '%v', return '0.0'", fp, jobMeta.ID)
} }
energyFootprint[fp] = energy energyFootprint[fp] = metricEnergy
totalEnergy += energy totalEnergy += metricEnergy
// log.Infof("Metric %s Average %f -> %f kWh | Job %d Total -> %f kWh", fp, LoadJobStat(jobMeta, fp, "avg"), energy, jobMeta.JobID, totalEnergy)
} }
var rawFootprint []byte var rawFootprint []byte
@ -620,7 +626,7 @@ func (r *JobRepository) UpdateEnergy(
return stmt, err return stmt, err
} }
return stmt.Set("energy_footprint", string(rawFootprint)).Set("energy", (math.Round(totalEnergy*100) / 100)), nil return stmt.Set("energy_footprint", string(rawFootprint)).Set("energy", (math.Round(totalEnergy*100.0) / 100.0)), nil
} }
func (r *JobRepository) UpdateFootprint( func (r *JobRepository) UpdateFootprint(

@ -176,6 +176,9 @@ func BuildWhereClause(filter *model.JobFilter, query sq.SelectBuilder) sq.Select
now := time.Now().Unix() // There does not seam to be a portable way to get the current unix timestamp accross different DBs. now := time.Now().Unix() // There does not seam to be a portable way to get the current unix timestamp accross different DBs.
query = query.Where("(job.job_state != 'running' OR (? - job.start_time) > ?)", now, *filter.MinRunningFor) query = query.Where("(job.job_state != 'running' OR (? - job.start_time) > ?)", now, *filter.MinRunningFor)
} }
if filter.Exclusive != nil {
query = query.Where("job.exclusive = ?", *filter.Exclusive)
}
if filter.State != nil { if filter.State != nil {
states := make([]string, len(filter.State)) states := make([]string, len(filter.State))
for i, val := range filter.State { for i, val := range filter.State {

@ -102,7 +102,7 @@ func GetHandle() ArchiveBackend {
return ar return ar
} }
// Helper to metricdata.LoadAverages(). // Helper to metricdataloader.LoadAverages().
func LoadAveragesFromArchive( func LoadAveragesFromArchive(
job *schema.Job, job *schema.Job,
metrics []string, metrics []string,
@ -125,6 +125,35 @@ func LoadAveragesFromArchive(
return nil return nil
} }
// Helper to metricdataloader.LoadStatData().
func LoadStatsFromArchive(
job *schema.Job,
metrics []string,
) (map[string]schema.MetricStatistics, error) {
data := make(map[string]schema.MetricStatistics, len(metrics))
metaFile, err := ar.LoadJobMeta(job)
if err != nil {
log.Warn("Error while loading job metadata from archiveBackend")
return data, err
}
for _, m := range metrics {
stat, ok := metaFile.Statistics[m]
if !ok {
data[m] = schema.MetricStatistics{Min: 0.0, Avg: 0.0, Max: 0.0}
continue
}
data[m] = schema.MetricStatistics{
Avg: stat.Avg,
Min: stat.Min,
Max: stat.Max,
}
}
return data, nil
}
func GetStatistics(job *schema.Job) (map[string]schema.JobStatistics, error) { func GetStatistics(job *schema.Job) (map[string]schema.JobStatistics, error) {
metaFile, err := ar.LoadJobMeta(job) metaFile, err := ar.LoadJobMeta(job)
if err != nil { if err != nil {

@ -20,6 +20,7 @@
Card, Card,
Table, Table,
Icon, Icon,
Tooltip
} from "@sveltestrap/sveltestrap"; } from "@sveltestrap/sveltestrap";
import { import {
init, init,
@ -70,6 +71,8 @@
...new Set([...metricsInHistograms, ...metricsInScatterplots.flat()]), ...new Set([...metricsInHistograms, ...metricsInScatterplots.flat()]),
]; ];
$: clusterName = cluster?.name ? cluster.name : cluster;
const sortOptions = [ const sortOptions = [
{ key: "totalWalltime", label: "Walltime" }, { key: "totalWalltime", label: "Walltime" },
{ key: "totalNodeHours", label: "Node Hours" }, { key: "totalNodeHours", label: "Node Hours" },
@ -159,6 +162,7 @@
groupBy: $groupBy groupBy: $groupBy
) { ) {
id id
name
totalWalltime totalWalltime
totalNodeHours totalNodeHours
totalCoreHours totalCoreHours
@ -422,15 +426,22 @@
<tr> <tr>
<td><Icon name="circle-fill" style="color: {colors[i]};" /></td> <td><Icon name="circle-fill" style="color: {colors[i]};" /></td>
{#if groupSelection.key == "user"} {#if groupSelection.key == "user"}
<th scope="col" <th scope="col" id="topName-{te.id}"
><a href="/monitoring/user/{te.id}?cluster={cluster}" ><a href="/monitoring/user/{te.id}?cluster={clusterName}"
>{te.id}</a >{te.id}</a
></th ></th
> >
{#if te?.name}
<Tooltip
target={`topName-${te.id}`}
placement="left"
>{te.name}</Tooltip
>
{/if}
{:else} {:else}
<th scope="col" <th scope="col"
><a ><a
href="/monitoring/jobs/?cluster={cluster}&project={te.id}&projectMatch=eq" href="/monitoring/jobs/?cluster={clusterName}&project={te.id}&projectMatch=eq"
>{te.id}</a >{te.id}</a
></th ></th
> >

@ -31,17 +31,16 @@
init, init,
groupByScope, groupByScope,
checkMetricDisabled, checkMetricDisabled,
transformDataForRoofline,
} from "./generic/utils.js"; } from "./generic/utils.js";
import Metric from "./job/Metric.svelte"; import Metric from "./job/Metric.svelte";
import StatsTable from "./job/StatsTable.svelte";
import JobSummary from "./job/JobSummary.svelte";
import EnergySummary from "./job/EnergySummary.svelte";
import ConcurrentJobs from "./generic/helper/ConcurrentJobs.svelte";
import PlotGrid from "./generic/PlotGrid.svelte";
import Roofline from "./generic/plots/Roofline.svelte";
import JobInfo from "./generic/joblist/JobInfo.svelte";
import MetricSelection from "./generic/select/MetricSelection.svelte"; import MetricSelection from "./generic/select/MetricSelection.svelte";
import JobInfo from "./generic/joblist/JobInfo.svelte";
import ConcurrentJobs from "./generic/helper/ConcurrentJobs.svelte";
import JobSummary from "./job/JobSummary.svelte";
import JobRoofline from "./job/JobRoofline.svelte";
import EnergySummary from "./job/EnergySummary.svelte";
import PlotGrid from "./generic/PlotGrid.svelte";
import StatsTable from "./job/StatsTable.svelte";
export let dbid; export let dbid;
export let username; export let username;
@ -57,10 +56,10 @@
selectedScopes = []; selectedScopes = [];
let plots = {}, let plots = {},
roofWidth,
statsTable statsTable
let missingMetrics = [], let availableMetrics = new Set(),
missingMetrics = [],
missingHosts = [], missingHosts = [],
somethingMissing = false; somethingMissing = false;
@ -117,43 +116,36 @@
} }
`; `;
const roofQuery = gql`
query ($dbid: ID!, $selectedMetrics: [String!]!, $selectedScopes: [MetricScope!]!, $selectedResolution: Int) {
jobMetrics(id: $dbid, metrics: $selectedMetrics, scopes: $selectedScopes, resolution: $selectedResolution) {
name
scope
metric {
series {
data
}
}
}
}
`;
$: jobMetrics = queryStore({ $: jobMetrics = queryStore({
client: client, client: client,
query: query, query: query,
variables: { dbid, selectedMetrics, selectedScopes }, variables: { dbid, selectedMetrics, selectedScopes },
}); });
// Roofline: Always load roofMetrics with configured timestep (Resolution: 0)
$: roofMetrics = queryStore({
client: client,
query: roofQuery,
variables: { dbid, selectedMetrics: ["flops_any", "mem_bw"], selectedScopes: ["node"], selectedResolution: 0 },
});
// Handle Job Query on Init -> is not executed anymore // Handle Job Query on Init -> is not executed anymore
getContext("on-init")(() => { getContext("on-init")(() => {
let job = $initq.data.job; let job = $initq.data.job;
if (!job) return; if (!job) return;
const pendingMetrics = [ const pendingMetrics = [
...(ccconfig[`job_view_selectedMetrics:${job.cluster}`] || ...(
ccconfig[`job_view_selectedMetrics`] (
ccconfig[`job_view_selectedMetrics:${job.cluster}:${job.subCluster}`] ||
ccconfig[`job_view_selectedMetrics:${job.cluster}`]
) ||
$initq.data.globalMetrics
.reduce((names, gm) => {
if (gm.availability.find((av) => av.cluster === job.cluster && av.subClusters.includes(job.subCluster))) {
names.push(gm.name);
}
return names;
}, [])
), ),
...(ccconfig[`job_view_nodestats_selectedMetrics:${job.cluster}`] || ...(
(
ccconfig[`job_view_nodestats_selectedMetrics:${job.cluster}:${job.subCluster}`] ||
ccconfig[`job_view_nodestats_selectedMetrics:${job.cluster}`]
) ||
ccconfig[`job_view_nodestats_selectedMetrics`] ccconfig[`job_view_nodestats_selectedMetrics`]
), ),
]; ];
@ -235,7 +227,7 @@ const roofQuery = gql`
</script> </script>
<Row class="mb-3"> <Row class="mb-3">
<!-- Column 1: Job Info, Job Tags, Concurrent Jobs, Admin Message if found--> <!-- Row 1, Column 1: Job Info, Job Tags, Concurrent Jobs, Admin Message if found-->
<Col xs={12} md={6} xl={3} class="mb-3 mb-xxl-0"> <Col xs={12} md={6} xl={3} class="mb-3 mb-xxl-0">
{#if $initq.error} {#if $initq.error}
<Card body color="danger">{$initq.error.message}</Card> <Card body color="danger">{$initq.error.message}</Card>
@ -277,51 +269,30 @@ const roofQuery = gql`
{/if} {/if}
</Col> </Col>
<!-- Column 2: Job Footprint, Polar Representation, Heuristic Summary --> <!-- Row 1, Column 2: Job Footprint, Polar Representation -->
<Col xs={12} md={6} xl={4} xxl={3} class="mb-3 mb-xxl-0"> <Col xs={12} md={6} xl={4} xxl={3} class="mb-3 mb-xxl-0">
{#if $initq.error} {#if $initq.error}
<Card body color="danger">{$initq.error.message}</Card> <Card body color="danger">{$initq.error.message}</Card>
{:else if $initq?.data && $jobMetrics?.data} {:else if $initq?.data}
<JobSummary job={$initq.data.job} jobMetrics={$jobMetrics.data.jobMetrics}/> <JobSummary job={$initq.data.job}/>
{:else} {:else}
<Spinner secondary /> <Spinner secondary />
{/if} {/if}
</Col> </Col>
<!-- Column 3: Job Roofline; If footprint Enabled: full width, else half width --> <!-- Row 1, Column 3: Job Roofline; If footprint Enabled: full width, else half width -->
<Col xs={12} md={12} xl={5} xxl={6}> <Col xs={12} md={12} xl={5} xxl={6}>
{#if $initq.error || $roofMetrics.error} {#if $initq.error}
<Card body color="danger"> <Card body color="danger">{$initq.error.message}</Card>
<p>Initq Error: {$initq.error?.message}</p> {:else if $initq?.data}
<p>roofMetrics (jobMetrics) Error: {$roofMetrics.error?.message}</p> <JobRoofline job={$initq.data.job} clusters={$initq.data.clusters}/>
</Card>
{:else if $initq?.data && $roofMetrics?.data}
<Card style="height: 400px;">
<div bind:clientWidth={roofWidth}>
<Roofline
allowSizeChange={true}
width={roofWidth}
renderTime={true}
subCluster={$initq.data.clusters
.find((c) => c.name == $initq.data.job.cluster)
.subClusters.find((sc) => sc.name == $initq.data.job.subCluster)}
data={transformDataForRoofline(
$roofMetrics.data?.jobMetrics?.find(
(m) => m.name == "flops_any" && m.scope == "node",
)?.metric,
$roofMetrics.data?.jobMetrics?.find(
(m) => m.name == "mem_bw" && m.scope == "node",
)?.metric,
)}
/>
</div>
</Card>
{:else} {:else}
<Spinner secondary /> <Spinner secondary />
{/if} {/if}
</Col> </Col>
</Row> </Row>
<!-- Row 2: Energy Information if available -->
{#if $initq?.data && $initq.data.job.energyFootprint.length != 0} {#if $initq?.data && $initq.data.job.energyFootprint.length != 0}
<Row class="mb-3"> <Row class="mb-3">
<Col> <Col>
@ -330,13 +301,14 @@ const roofQuery = gql`
</Row> </Row>
{/if} {/if}
<!-- Metric Plot Grid -->
<Card class="mb-3"> <Card class="mb-3">
<CardBody> <CardBody>
<Row class="mb-2"> <Row class="mb-2">
{#if $initq.data} {#if $initq.data}
<Col xs="auto"> <Col xs="auto">
<Button outline on:click={() => (isMetricsSelectionOpen = true)} color="primary"> <Button outline on:click={() => (isMetricsSelectionOpen = true)} color="primary">
Select Metrics Select Metrics (Selected {selectedMetrics.length} of {availableMetrics.size} available)
</Button> </Button>
</Col> </Col>
{/if} {/if}
@ -390,6 +362,7 @@ const roofQuery = gql`
</CardBody> </CardBody>
</Card> </Card>
<!-- Statistcics Table -->
<Row class="mb-3"> <Row class="mb-3">
<Col> <Col>
{#if $initq.data} {#if $initq.data}
@ -470,9 +443,11 @@ const roofQuery = gql`
{#if $initq.data} {#if $initq.data}
<MetricSelection <MetricSelection
cluster={$initq.data.job.cluster} cluster={$initq.data.job.cluster}
subCluster={$initq.data.job.subCluster}
configName="job_view_selectedMetrics" configName="job_view_selectedMetrics"
bind:metrics={selectedMetrics} bind:metrics={selectedMetrics}
bind:isOpen={isMetricsSelectionOpen} bind:isOpen={isMetricsSelectionOpen}
bind:allMetrics={availableMetrics}
/> />
{/if} {/if}

@ -137,5 +137,5 @@
bind:metrics bind:metrics
bind:isOpen={isMetricsSelectionOpen} bind:isOpen={isMetricsSelectionOpen}
bind:showFootprint bind:showFootprint
footprintSelect={true} footprintSelect
/> />

@ -19,6 +19,7 @@
Progress, Progress,
Icon, Icon,
Button, Button,
Tooltip
} from "@sveltestrap/sveltestrap"; } from "@sveltestrap/sveltestrap";
import { import {
queryStore, queryStore,
@ -75,9 +76,9 @@
); );
let isHistogramSelectionOpen = false; let isHistogramSelectionOpen = false;
$: metricsInHistograms = cluster $: selectedHistograms = cluster
? ccconfig[`user_view_histogramMetrics:${cluster}`] || [] ? ccconfig[`user_view_histogramMetrics:${cluster}`] || ( ccconfig['user_view_histogramMetrics'] || [] )
: ccconfig.user_view_histogramMetrics || []; : ccconfig['user_view_histogramMetrics'] || [];
const client = getContextClient(); const client = getContextClient();
// Note: nodeMetrics are requested on configured $timestep resolution // Note: nodeMetrics are requested on configured $timestep resolution
@ -90,7 +91,7 @@
$metrics: [String!] $metrics: [String!]
$from: Time! $from: Time!
$to: Time! $to: Time!
$metricsInHistograms: [String!] $selectedHistograms: [String!]
) { ) {
nodeMetrics( nodeMetrics(
cluster: $cluster cluster: $cluster
@ -116,7 +117,7 @@
} }
} }
stats: jobsStatistics(filter: $filter, metrics: $metricsInHistograms) { stats: jobsStatistics(filter: $filter, metrics: $selectedHistograms) {
histDuration { histDuration {
count count
value value
@ -157,7 +158,7 @@
from: from.toISOString(), from: from.toISOString(),
to: to.toISOString(), to: to.toISOString(),
filter: [{ state: ["running"] }, { cluster: { eq: cluster } }], filter: [{ state: ["running"] }, { cluster: { eq: cluster } }],
metricsInHistograms: metricsInHistograms, selectedHistograms: selectedHistograms,
}, },
}); });
@ -177,6 +178,7 @@
groupBy: USER groupBy: USER
) { ) {
id id
name
totalJobs totalJobs
totalNodes totalNodes
totalCores totalCores
@ -515,12 +517,19 @@
{#each $topUserQuery.data.topUser as tu, i} {#each $topUserQuery.data.topUser as tu, i}
<tr> <tr>
<td><Icon name="circle-fill" style="color: {colors[i]};" /></td> <td><Icon name="circle-fill" style="color: {colors[i]};" /></td>
<th scope="col" <th scope="col" id="topName-{tu.id}"
><a ><a
href="/monitoring/user/{tu.id}?cluster={cluster}&state=running" href="/monitoring/user/{tu.id}?cluster={cluster}&state=running"
>{tu.id}</a >{tu.id}</a
></th ></th
> >
{#if tu?.name}
<Tooltip
target={`topName-${tu.id}`}
placement="left"
>{tu.name}</Tooltip
>
{/if}
<td>{tu[topUserSelection.key]}</td> <td>{tu[topUserSelection.key]}</td>
</tr> </tr>
{/each} {/each}
@ -652,7 +661,7 @@
<!-- Selectable Stats as Histograms : Average Values of Running Jobs --> <!-- Selectable Stats as Histograms : Average Values of Running Jobs -->
{#if metricsInHistograms} {#if selectedHistograms}
{#key $mainQuery.data.stats[0].histMetrics} {#key $mainQuery.data.stats[0].histMetrics}
<PlotGrid <PlotGrid
let:item let:item
@ -675,6 +684,6 @@
<HistogramSelection <HistogramSelection
bind:cluster bind:cluster
bind:metricsInHistograms bind:selectedHistograms
bind:isOpen={isHistogramSelectionOpen} bind:isOpen={isHistogramSelectionOpen}
/> />

@ -29,8 +29,8 @@
import Refresher from "./generic/helper/Refresher.svelte"; import Refresher from "./generic/helper/Refresher.svelte";
export let displayType; export let displayType;
export let cluster; export let cluster = null;
export let subCluster = ""; export let subCluster = null;
export let from = null; export let from = null;
export let to = null; export let to = null;
@ -60,7 +60,10 @@
let hostnameFilter = ""; let hostnameFilter = "";
let pendingHostnameFilter = ""; let pendingHostnameFilter = "";
let selectedMetric = ccconfig.system_view_selectedMetric || ""; let selectedMetric = ccconfig.system_view_selectedMetric || "";
let selectedMetrics = ccconfig[`node_list_selectedMetrics:${cluster}`] || [ccconfig.system_view_selectedMetric]; let selectedMetrics = (
ccconfig[`node_list_selectedMetrics:${cluster}:${subCluster}`] ||
ccconfig[`node_list_selectedMetrics:${cluster}`]
) || [ccconfig.system_view_selectedMetric];
let isMetricsSelectionOpen = false; let isMetricsSelectionOpen = false;
/* /*
@ -191,6 +194,7 @@
<MetricSelection <MetricSelection
{cluster} {cluster}
{subCluster}
configName="node_list_selectedMetrics" configName="node_list_selectedMetrics"
metrics={selectedMetrics} metrics={selectedMetrics}
bind:isOpen={isMetricsSelectionOpen} bind:isOpen={isMetricsSelectionOpen}

@ -68,16 +68,16 @@
let durationBinOptions = ["1m","10m","1h","6h","12h"]; let durationBinOptions = ["1m","10m","1h","6h","12h"];
let metricBinOptions = [10, 20, 50, 100]; let metricBinOptions = [10, 20, 50, 100];
$: metricsInHistograms = selectedCluster $: selectedHistograms = selectedCluster
? ccconfig[`user_view_histogramMetrics:${selectedCluster}`] || [] ? ccconfig[`user_view_histogramMetrics:${selectedCluster}`] || ( ccconfig['user_view_histogramMetrics'] || [] )
: ccconfig.user_view_histogramMetrics || []; : ccconfig['user_view_histogramMetrics'] || [];
const client = getContextClient(); const client = getContextClient();
$: stats = queryStore({ $: stats = queryStore({
client: client, client: client,
query: gql` query: gql`
query ($jobFilters: [JobFilter!]!, $metricsInHistograms: [String!], $numDurationBins: String, $numMetricBins: Int) { query ($jobFilters: [JobFilter!]!, $selectedHistograms: [String!], $numDurationBins: String, $numMetricBins: Int) {
jobsStatistics(filter: $jobFilters, metrics: $metricsInHistograms, numDurationBins: $numDurationBins , numMetricBins: $numMetricBins ) { jobsStatistics(filter: $jobFilters, metrics: $selectedHistograms, numDurationBins: $numDurationBins , numMetricBins: $numMetricBins ) {
totalJobs totalJobs
shortJobs shortJobs
totalWalltime totalWalltime
@ -104,7 +104,7 @@
} }
} }
`, `,
variables: { jobFilters, metricsInHistograms, numDurationBins, numMetricBins }, variables: { jobFilters, selectedHistograms, numDurationBins, numMetricBins },
}); });
onMount(() => filterComponent.updateFilters()); onMount(() => filterComponent.updateFilters());
@ -290,7 +290,7 @@
</InputGroup> </InputGroup>
</Col> </Col>
</Row> </Row>
{#if metricsInHistograms?.length > 0} {#if selectedHistograms?.length > 0}
{#if $stats.error} {#if $stats.error}
<Row> <Row>
<Col> <Col>
@ -352,11 +352,11 @@
bind:metrics bind:metrics
bind:isOpen={isMetricsSelectionOpen} bind:isOpen={isMetricsSelectionOpen}
bind:showFootprint bind:showFootprint
footprintSelect={true} footprintSelect
/> />
<HistogramSelection <HistogramSelection
bind:cluster={selectedCluster} bind:cluster={selectedCluster}
bind:metricsInHistograms bind:selectedHistograms
bind:isOpen={isHistogramSelectionOpen} bind:isOpen={isHistogramSelectionOpen}
/> />

@ -45,6 +45,14 @@
export let startTimeQuickSelect = false; export let startTimeQuickSelect = false;
export let matchedJobs = -2; export let matchedJobs = -2;
const startTimeSelectOptions = [
{ range: "", rangeLabel: "No Selection"},
{ range: "last6h", rangeLabel: "Last 6hrs"},
{ range: "last24h", rangeLabel: "Last 24hrs"},
{ range: "last7d", rangeLabel: "Last 7 days"},
{ range: "last30d", rangeLabel: "Last 30 days"}
];
let filters = { let filters = {
projectMatch: filterPresets.projectMatch || "contains", projectMatch: filterPresets.projectMatch || "contains",
userMatch: filterPresets.userMatch || "contains", userMatch: filterPresets.userMatch || "contains",
@ -56,7 +64,7 @@
filterPresets.states || filterPresets.state filterPresets.states || filterPresets.state
? [filterPresets.state].flat() ? [filterPresets.state].flat()
: allJobStates, : allJobStates,
startTime: filterPresets.startTime || { from: null, to: null }, startTime: filterPresets.startTime || { from: null, to: null, range: ""},
tags: filterPresets.tags || [], tags: filterPresets.tags || [],
duration: filterPresets.duration || { duration: filterPresets.duration || {
lessThan: null, lessThan: null,
@ -268,16 +276,17 @@
{#if startTimeQuickSelect} {#if startTimeQuickSelect}
<DropdownItem divider /> <DropdownItem divider />
<DropdownItem disabled>Start Time Quick Selection</DropdownItem> <DropdownItem disabled>Start Time Quick Selection</DropdownItem>
{#each [{ text: "Last 6hrs", range: "last6h" }, { text: "Last 24hrs", range: "last24h" }, { text: "Last 7 days", range: "last7d" }, { text: "Last 30 days", range: "last30d" }] as { text, range }} {#each startTimeSelectOptions.filter((stso) => stso.range !== "") as { rangeLabel, range }}
<DropdownItem <DropdownItem
on:click={() => { on:click={() => {
filters.startTime.from = null
filters.startTime.to = null
filters.startTime.range = range; filters.startTime.range = range;
filters.startTime.text = text;
updateFilters(); updateFilters();
}} }}
> >
<Icon name="calendar-range" /> <Icon name="calendar-range" />
{text} {rangeLabel}
</DropdownItem> </DropdownItem>
{/each} {/each}
{/if} {/if}
@ -316,7 +325,7 @@
{#if filters.startTime.range} {#if filters.startTime.range}
<Info icon="calendar-range" on:click={() => (isStartTimeOpen = true)}> <Info icon="calendar-range" on:click={() => (isStartTimeOpen = true)}>
{filters?.startTime?.text ? filters.startTime.text : filters.startTime.range } {startTimeSelectOptions.find((stso) => stso.range === filters.startTime.range).rangeLabel }
</Info> </Info>
{/if} {/if}
@ -414,11 +423,8 @@
bind:from={filters.startTime.from} bind:from={filters.startTime.from}
bind:to={filters.startTime.to} bind:to={filters.startTime.to}
bind:range={filters.startTime.range} bind:range={filters.startTime.range}
on:set-filter={() => { {startTimeSelectOptions}
delete filters.startTime["text"]; on:set-filter={() => updateFilters()}
delete filters.startTime["range"];
updateFilters();
}}
/> />
<Duration <Duration

@ -43,6 +43,10 @@
<ModalBody> <ModalBody>
{#if $initialized} {#if $initialized}
<h4>Cluster</h4> <h4>Cluster</h4>
{#if disableClusterSelection}
<Button color="info" class="w-100 mb-2" disabled><b>Info: Cluster Selection Disabled in This View</b></Button>
<Button outline color="primary" class="w-100 mb-2" disabled><b>Selected Cluster: {cluster}</b></Button>
{:else}
<ListGroup> <ListGroup>
<ListGroupItem <ListGroupItem
disabled={disableClusterSelection} disabled={disableClusterSelection}
@ -64,6 +68,7 @@
{/each} {/each}
</ListGroup> </ListGroup>
{/if} {/if}
{/if}
{#if $initialized && pendingCluster != null} {#if $initialized && pendingCluster != null}
<br /> <br />
<h4>Partiton</h4> <h4>Partiton</h4>

@ -17,7 +17,6 @@
import { parse, format, sub } from "date-fns"; import { parse, format, sub } from "date-fns";
import { import {
Row, Row,
Col,
Button, Button,
Input, Input,
Modal, Modal,
@ -34,8 +33,7 @@
export let from = null; export let from = null;
export let to = null; export let to = null;
export let range = ""; export let range = "";
export let startTimeSelectOptions;
let pendingFrom, pendingTo;
const now = new Date(Date.now()); const now = new Date(Date.now());
const ago = sub(now, { months: 1 }); const ago = sub(now, { months: 1 });
@ -48,12 +46,24 @@
time: format(now, "HH:mm"), time: format(now, "HH:mm"),
}; };
function reset() { $: pendingFrom = (from == null) ? defaultFrom : fromRFC3339(from)
pendingFrom = from == null ? defaultFrom : fromRFC3339(from); $: pendingTo = (to == null) ? defaultTo : fromRFC3339(to)
pendingTo = to == null ? defaultTo : fromRFC3339(to); $: pendingRange = range
}
reset(); $: isModified =
(from != toRFC3339(pendingFrom) || to != toRFC3339(pendingTo, "59")) &&
(range != pendingRange) &&
!(
from == null &&
pendingFrom.date == "0000-00-00" &&
pendingFrom.time == "00:00"
) &&
!(
to == null &&
pendingTo.date == "0000-00-00" &&
pendingTo.time == "00:00"
) &&
!( range == "" && pendingRange == "");
function toRFC3339({ date, time }, secs = "00") { function toRFC3339({ date, time }, secs = "00") {
const parsedDate = parse( const parsedDate = parse(
@ -71,19 +81,6 @@
time: format(parsedDate, "HH:mm"), time: format(parsedDate, "HH:mm"),
}; };
} }
$: isModified =
(from != toRFC3339(pendingFrom) || to != toRFC3339(pendingTo, "59")) &&
!(
from == null &&
pendingFrom.date == "0000-00-00" &&
pendingFrom.time == "00:00"
) &&
!(
to == null &&
pendingTo.date == "0000-00-00" &&
pendingTo.time == "00:00"
);
</script> </script>
<Modal {isOpen} toggle={() => (isOpen = !isOpen)}> <Modal {isOpen} toggle={() => (isOpen = !isOpen)}>
@ -92,31 +89,59 @@
{#if range !== ""} {#if range !== ""}
<h4>Current Range</h4> <h4>Current Range</h4>
<Row> <Row>
<Col> <FormGroup class="col">
<Input type="text" value={range} disabled/> <Input type ="select" bind:value={pendingRange} >
</Col> {#each startTimeSelectOptions as { rangeLabel, range }}
<option label={rangeLabel} value={range}/>
{/each}
</Input>
</FormGroup>
</Row> </Row>
{/if} {/if}
<h4>From</h4> <h4>From</h4>
<Row> <Row>
<FormGroup class="col"> <FormGroup class="col">
<Input type="date" bind:value={pendingFrom.date} /> <Input type="date" bind:value={pendingFrom.date} disabled={pendingRange !== ""}/>
</FormGroup> </FormGroup>
<FormGroup class="col"> <FormGroup class="col">
<Input type="time" bind:value={pendingFrom.time} /> <Input type="time" bind:value={pendingFrom.time} disabled={pendingRange !== ""}/>
</FormGroup> </FormGroup>
</Row> </Row>
<h4>To</h4> <h4>To</h4>
<Row> <Row>
<FormGroup class="col"> <FormGroup class="col">
<Input type="date" bind:value={pendingTo.date} /> <Input type="date" bind:value={pendingTo.date} disabled={pendingRange !== ""}/>
</FormGroup> </FormGroup>
<FormGroup class="col"> <FormGroup class="col">
<Input type="time" bind:value={pendingTo.time} /> <Input type="time" bind:value={pendingTo.time} disabled={pendingRange !== ""}/>
</FormGroup> </FormGroup>
</Row> </Row>
</ModalBody> </ModalBody>
<ModalFooter> <ModalFooter>
{#if pendingRange !== ""}
<Button
color="warning"
disabled={pendingRange === ""}
on:click={() => {
pendingRange = ""
}}
>
Reset Range
</Button>
<Button
color="primary"
disabled={pendingRange === ""}
on:click={() => {
isOpen = false;
from = null;
to = null;
range = pendingRange;
dispatch("set-filter", { from, to, range });
}}
>
Close & Apply Range
</Button>
{:else}
<Button <Button
color="primary" color="primary"
disabled={pendingFrom.date == "0000-00-00" || disabled={pendingFrom.date == "0000-00-00" ||
@ -125,19 +150,21 @@
isOpen = false; isOpen = false;
from = toRFC3339(pendingFrom); from = toRFC3339(pendingFrom);
to = toRFC3339(pendingTo, "59"); to = toRFC3339(pendingTo, "59");
dispatch("set-filter", { from, to }); range = "";
dispatch("set-filter", { from, to, range });
}} }}
> >
Close & Apply Close & Apply Dates
</Button> </Button>
{/if}
<Button <Button
color="danger" color="danger"
on:click={() => { on:click={() => {
isOpen = false; isOpen = false;
from = null; from = null;
to = null; to = null;
reset(); range = "";
dispatch("set-filter", { from, to }); dispatch("set-filter", { from, to, range });
}}>Reset</Button }}>Reset</Button
> >
<Button on:click={() => (isOpen = false)}>Close</Button> <Button on:click={() => (isOpen = false)}>Close</Button>

@ -3,7 +3,7 @@
Properties: Properties:
- `polarMetrics [Object]?`: Metric names and scaled peak values for rendering polar plot [Default: [] ] - `polarMetrics [Object]?`: Metric names and scaled peak values for rendering polar plot [Default: [] ]
- `jobMetrics [GraphQL.JobMetricWithName]?`: Metric data [Default: null] - `polarData [GraphQL.JobMetricStatWithName]?`: Metric data [Default: null]
- `height Number?`: Plot height [Default: 365] - `height Number?`: Plot height [Default: 365]
--> -->
@ -31,7 +31,7 @@
); );
export let polarMetrics = []; export let polarMetrics = [];
export let jobMetrics = null; export let polarData = [];
export let height = 350; export let height = 350;
const labels = polarMetrics const labels = polarMetrics
@ -40,53 +40,26 @@
.sort(function (a, b) {return ((a > b) ? 1 : ((b > a) ? -1 : 0))}); .sort(function (a, b) {return ((a > b) ? 1 : ((b > a) ? -1 : 0))});
function loadData(type) { function loadData(type) {
if (!labels) { if (labels && (type == 'avg' || type == 'min' ||type == 'max')) {
return getValues(type)
} else if (!labels) {
console.warn("Empty 'polarMetrics' array prop! Cannot render Polar representation.") console.warn("Empty 'polarMetrics' array prop! Cannot render Polar representation.")
return []
} else { } else {
if (type === 'avg') { console.warn('Unknown Type For Polar Data (must be one of [min, max, avg])')
return getValues(getAvg)
} else if (type === 'max') {
return getValues(getMax)
} else if (type === 'min') {
return getValues(getMin)
} }
console.log('Unknown Type For Polar Data (must be one of [min, max, avg])')
return [] return []
} }
}
// Helpers // Helper
const getValues = (getStat) => labels.map(name => { const getValues = (type) => labels.map(name => {
// Peak is adapted and scaled for job shared state // Peak is adapted and scaled for job shared state
const peak = polarMetrics.find(m => m.name == name).peak const peak = polarMetrics.find(m => m?.name == name)?.peak
const metric = jobMetrics.find(m => m.name == name && m.scope == "node") const metric = polarData.find(m => m?.name == name)?.stats
const value = getStat(metric.metric) / peak const value = (peak && metric) ? (metric[type] / peak) : 0
return value <= 1. ? value : 1. return value <= 1. ? value : 1.
}) })
function getMax(metric) {
let max = metric.series[0].statistics.max;
for (let series of metric.series)
max = Math.max(max, series.statistics.max)
return max
}
function getMin(metric) {
let min = metric.series[0].statistics.min;
for (let series of metric.series)
min = Math.min(min, series.statistics.min)
return min
}
function getAvg(metric) {
let avg = 0;
for (let series of metric.series)
avg += series.statistics.avg
return avg / metric.series.length
}
// Chart JS Objects // Chart JS Objects
const data = { const data = {

@ -179,7 +179,7 @@
function render(plotData) { function render(plotData) {
if (plotData) { if (plotData) {
const opts = { const opts = {
title: "", title: "CPU Roofline Diagram",
mode: 2, mode: 2,
width: width, width: width,
height: height, height: height,

@ -3,7 +3,7 @@
Properties: Properties:
- `cluster String`: Currently selected cluster - `cluster String`: Currently selected cluster
- `metricsInHistograms [String]`: The currently selected metrics to display as histogram - `selectedHistograms [String]`: The currently selected metrics to display as histogram
- ìsOpen Bool`: Is selection opened - ìsOpen Bool`: Is selection opened
--> -->
@ -21,21 +21,26 @@
import { gql, getContextClient, mutationStore } from "@urql/svelte"; import { gql, getContextClient, mutationStore } from "@urql/svelte";
export let cluster; export let cluster;
export let metricsInHistograms; export let selectedHistograms;
export let isOpen; export let isOpen;
const client = getContextClient(); const client = getContextClient();
const initialized = getContext("initialized"); const initialized = getContext("initialized");
let availableMetrics = [] function loadHistoMetrics(isInitialized, thisCluster) {
if (!isInitialized) return [];
function loadHistoMetrics(isInitialized) { if (!thisCluster) {
if (!isInitialized) return; return getContext("globalMetrics")
const rawAvailableMetrics = getContext("globalMetrics").filter((gm) => gm?.footprint).map((fgm) => { return fgm.name }) .filter((gm) => gm?.footprint)
availableMetrics = [...rawAvailableMetrics] .map((fgm) => { return fgm.name })
} else {
return getContext("globalMetrics")
.filter((gm) => gm?.availability.find((av) => av.cluster == thisCluster))
.filter((agm) => agm?.footprint)
.map((afgm) => { return afgm.name })
}
} }
let pendingMetrics = [...metricsInHistograms]; // Copy
const updateConfigurationMutation = ({ name, value }) => { const updateConfigurationMutation = ({ name, value }) => {
return mutationStore({ return mutationStore({
@ -61,17 +66,16 @@
} }
function closeAndApply() { function closeAndApply() {
metricsInHistograms = [...pendingMetrics]; // Set for parent
isOpen = !isOpen; isOpen = !isOpen;
updateConfiguration({ updateConfiguration({
name: cluster name: cluster
? `user_view_histogramMetrics:${cluster}` ? `user_view_histogramMetrics:${cluster}`
: "user_view_histogramMetrics", : "user_view_histogramMetrics",
value: metricsInHistograms, value: selectedHistograms,
}); });
} }
$: loadHistoMetrics($initialized); $: availableMetrics = loadHistoMetrics($initialized, cluster);
</script> </script>
@ -81,7 +85,7 @@
<ListGroup> <ListGroup>
{#each availableMetrics as metric (metric)} {#each availableMetrics as metric (metric)}
<ListGroupItem> <ListGroupItem>
<input type="checkbox" bind:group={pendingMetrics} value={metric} /> <input type="checkbox" bind:group={selectedHistograms} value={metric} />
{metric} {metric}
</ListGroupItem> </ListGroupItem>
{/each} {/each}

@ -28,6 +28,7 @@
export let configName; export let configName;
export let allMetrics = null; export let allMetrics = null;
export let cluster = null; export let cluster = null;
export let subCluster = null;
export let showFootprint = false; export let showFootprint = false;
export let footprintSelect = false; export let footprintSelect = false;
@ -46,12 +47,16 @@
$: { $: {
if (allMetrics != null) { if (allMetrics != null) {
if (cluster == null) { if (!cluster) {
for (let metric of globalMetrics) allMetrics.add(metric.name); for (let metric of globalMetrics) allMetrics.add(metric.name);
} else { } else {
allMetrics.clear(); allMetrics.clear();
for (let gm of globalMetrics) { for (let gm of globalMetrics) {
if (!subCluster) {
if (gm.availability.find((av) => av.cluster === cluster)) allMetrics.add(gm.name); if (gm.availability.find((av) => av.cluster === cluster)) allMetrics.add(gm.name);
} else {
if (gm.availability.find((av) => av.cluster === cluster && av.subClusters.includes(subCluster))) allMetrics.add(gm.name);
}
} }
} }
newMetricsOrder = [...allMetrics].filter((m) => !metrics.includes(m)); newMetricsOrder = [...allMetrics].filter((m) => !metrics.includes(m));
@ -62,7 +67,7 @@
function printAvailability(metric, cluster) { function printAvailability(metric, cluster) {
const avail = globalMetrics.find((gm) => gm.name === metric)?.availability const avail = globalMetrics.find((gm) => gm.name === metric)?.availability
if (cluster == null) { if (!cluster) {
return avail.map((av) => av.cluster).join(',') return avail.map((av) => av.cluster).join(',')
} else { } else {
return avail.find((av) => av.cluster === cluster).subClusters.join(',') return avail.find((av) => av.cluster === cluster).subClusters.join(',')
@ -107,10 +112,17 @@
metrics = newMetricsOrder.filter((m) => unorderedMetrics.includes(m)); metrics = newMetricsOrder.filter((m) => unorderedMetrics.includes(m));
isOpen = false; isOpen = false;
showFootprint = !!pendingShowFootprint; let configKey;
if (cluster && subCluster) {
configKey = `${configName}:${cluster}:${subCluster}`;
} else if (cluster && !subCluster) {
configKey = `${configName}:${cluster}`;
} else {
configKey = `${configName}`;
}
updateConfigurationMutation({ updateConfigurationMutation({
name: cluster == null ? configName : `${configName}:${cluster}`, name: configKey,
value: JSON.stringify(metrics), value: JSON.stringify(metrics),
}).subscribe((res) => { }).subscribe((res) => {
if (res.fetching === false && res.error) { if (res.fetching === false && res.error) {
@ -118,9 +130,11 @@
} }
}); });
if (footprintSelect) {
showFootprint = !!pendingShowFootprint;
updateConfigurationMutation({ updateConfigurationMutation({
name: name:
cluster == null !cluster
? "plot_list_showFootprint" ? "plot_list_showFootprint"
: `plot_list_showFootprint:${cluster}`, : `plot_list_showFootprint:${cluster}`,
value: JSON.stringify(showFootprint), value: JSON.stringify(showFootprint),
@ -129,6 +143,7 @@
throw res.error; throw res.error;
} }
}); });
};
dispatch('update-metrics', metrics); dispatch('update-metrics', metrics);
} }

@ -0,0 +1,79 @@
<!--
@component Job View Roofline component; Queries data for and renders roofline plot.
Properties:
- `job Object`: The GQL job object
- `clusters Array`: The GQL clusters array
-->
<script>
import {
queryStore,
gql,
getContextClient
} from "@urql/svelte";
import {
Card,
Spinner
} from "@sveltestrap/sveltestrap";
import {
transformDataForRoofline,
} from "../generic/utils.js";
import Roofline from "../generic/plots/Roofline.svelte";
export let job;
export let clusters;
let roofWidth;
const client = getContextClient();
const roofQuery = gql`
query ($dbid: ID!, $selectedMetrics: [String!]!, $selectedScopes: [MetricScope!]!, $selectedResolution: Int) {
jobMetrics(id: $dbid, metrics: $selectedMetrics, scopes: $selectedScopes, resolution: $selectedResolution) {
name
scope
metric {
series {
data
}
}
}
}
`;
// Roofline: Always load roofMetrics with configured timestep (Resolution: 0)
$: roofMetrics = queryStore({
client: client,
query: roofQuery,
variables: { dbid: job.id, selectedMetrics: ["flops_any", "mem_bw"], selectedScopes: ["node"], selectedResolution: 0 },
});
</script>
{#if $roofMetrics.error}
<Card body color="danger">{$roofMetrics.error.message}</Card>
{:else if $roofMetrics?.data}
<Card style="height: 400px;">
<div bind:clientWidth={roofWidth}>
<Roofline
width={roofWidth}
subCluster={clusters
.find((c) => c.name == job.cluster)
.subClusters.find((sc) => sc.name == job.subCluster)}
data={transformDataForRoofline(
$roofMetrics.data?.jobMetrics?.find(
(m) => m.name == "flops_any" && m.scope == "node",
)?.metric,
$roofMetrics.data?.jobMetrics?.find(
(m) => m.name == "mem_bw" && m.scope == "node",
)?.metric,
)}
allowSizeChange
renderTime
/>
</div>
</Card>
{:else}
<Spinner secondary />
{/if}

@ -1,9 +1,8 @@
<!-- <!--
@component Job Summary component; Displays job.footprint data as bars in relation to thresholds, as polar plot, and summariziong comment @component Job Summary component; Displays aggregated job footprint statistics and performance indicators
Properties: Properties:
- `job Object`: The GQL job object - `job Object`: The GQL job object
- `displayTitle Bool?`: If to display cardHeader with title [Default: true]
- `width String?`: Width of the card [Default: 'auto'] - `width String?`: Width of the card [Default: 'auto']
- `height String?`: Height of the card [Default: '310px'] - `height String?`: Height of the card [Default: '310px']
--> -->
@ -12,317 +11,31 @@
import { getContext } from "svelte"; import { getContext } from "svelte";
import { import {
Card, Card,
CardBody,
Progress,
Icon,
Tooltip,
Row,
Col,
TabContent, TabContent,
TabPane TabPane
} from "@sveltestrap/sveltestrap"; } from "@sveltestrap/sveltestrap";
import Polar from "../generic/plots/Polar.svelte"; import JobFootprintBars from "./jobsummary/JobFootprintBars.svelte";
import { findJobFootprintThresholds } from "../generic/utils.js"; import JobFootprintPolar from "./jobsummary/JobFootprintPolar.svelte";
export let job; export let job;
export let jobMetrics;
export let width = "auto"; export let width = "auto";
export let height = "400px"; export let height = "400px";
const ccconfig = getContext("cc-config") const showFootprintTab = !!getContext("cc-config")[`job_view_showFootprint`];
const globalMetrics = getContext("globalMetrics")
const showFootprintTab = !!ccconfig[`job_view_showFootprint`];
// Metrics Configured To Be Footprints For (sub)Cluster
const clusterFootprintMetrics = getContext("clusters")
.find((c) => c.name == job.cluster)?.subClusters
.find((sc) => sc.name == job.subCluster)?.footprint || []
// Data For Polarplot Will Be Calculated Based On JobMetrics And Thresholds
const polarMetrics = globalMetrics.reduce((pms, gm) => {
if (clusterFootprintMetrics.includes(gm.name)) {
const fmt = findJobFootprintThresholds(job, gm.footprint, getContext("getMetricConfig")(job.cluster, job.subCluster, gm.name));
pms.push({ name: gm.name, peak: fmt ? fmt.peak : null });
}
return pms;
}, [])
// Prepare Job Footprint Data Based On Values Saved In Database
const jobFootprintData = !showFootprintTab ? null : job?.footprint?.map((jf) => {
const fmc = getContext("getMetricConfig")(job.cluster, job.subCluster, jf.name);
if (fmc) {
// Unit
const unit = (fmc?.unit?.prefix ? fmc.unit.prefix : "") + (fmc?.unit?.base ? fmc.unit.base : "")
// Threshold / -Differences
const fmt = findJobFootprintThresholds(job, jf.stat, fmc);
// Define basic data -> Value: Use as Provided
const fmBase = {
name: jf.name,
stat: jf.stat,
value: jf.value,
unit: unit,
peak: fmt.peak,
dir: fmc.lowerIsBetter
};
if (evalFootprint(jf.value, fmt, fmc.lowerIsBetter, "alert")) {
return {
...fmBase,
color: "danger",
message: `Footprint value way ${fmc.lowerIsBetter ? "above" : "below"} expected normal threshold.`,
impact: 3
};
} else if (evalFootprint(jf.value, fmt, fmc.lowerIsBetter, "caution")) {
return {
...fmBase,
color: "warning",
message: `Footprint value ${fmc.lowerIsBetter ? "above" : "below"} expected normal threshold.`,
impact: 2,
};
} else if (evalFootprint(jf.value, fmt, fmc.lowerIsBetter, "normal")) {
return {
...fmBase,
color: "success",
message: "Footprint value within expected thresholds.",
impact: 1,
};
} else if (evalFootprint(jf.value, fmt, fmc.lowerIsBetter, "peak")) {
return {
...fmBase,
color: "info",
message:
"Footprint value above expected normal threshold: Check for artifacts recommended.",
impact: 0,
};
} else {
return {
...fmBase,
color: "secondary",
message:
"Footprint value above expected peak threshold: Check for artifacts!",
impact: -1,
};
}
} else { // No matching metric config: display as single value
return {
name: jf.name,
stat: jf.stat,
value: jf.value,
message:
`No config for metric ${jf.name} found.`,
impact: 4,
};
}
}).sort(function (a, b) { // Sort by impact value primarily, within impact sort name alphabetically
return a.impact - b.impact || ((a.name > b.name) ? 1 : ((b.name > a.name) ? -1 : 0));
});;
function evalFootprint(value, thresholds, lowerIsBetter, level) {
// Handle Metrics in which less value is better
switch (level) {
case "peak":
if (lowerIsBetter)
return false; // metric over peak -> return false to trigger impact -1
else return value <= thresholds.peak && value > thresholds.normal;
case "alert":
if (lowerIsBetter)
return value <= thresholds.peak && value >= thresholds.alert;
else return value <= thresholds.alert && value >= 0;
case "caution":
if (lowerIsBetter)
return value < thresholds.alert && value >= thresholds.caution;
else return value <= thresholds.caution && value > thresholds.alert;
case "normal":
if (lowerIsBetter)
return value < thresholds.caution && value >= 0;
else return value <= thresholds.normal && value > thresholds.caution;
default:
return false;
}
}
/*
function writeSummary(fpd) {
// Hardcoded! Needs to be retrieved from globalMetrics
const performanceMetrics = ['flops_any', 'mem_bw'];
const utilizationMetrics = ['cpu_load', 'acc_utilization'];
const energyMetrics = ['cpu_power'];
let performanceScore = 0;
let utilizationScore = 0;
let energyScore = 0;
let performanceMetricsCounted = 0;
let utilizationMetricsCounted = 0;
let energyMetricsCounted = 0;
fpd.forEach(metric => {
console.log('Metric, Impact', metric.name, metric.impact)
if (performanceMetrics.includes(metric.name)) {
performanceScore += metric.impact
performanceMetricsCounted += 1
} else if (utilizationMetrics.includes(metric.name)) {
utilizationScore += metric.impact
utilizationMetricsCounted += 1
} else if (energyMetrics.includes(metric.name)) {
energyScore += metric.impact
energyMetricsCounted += 1
}
});
performanceScore = (performanceMetricsCounted == 0) ? performanceScore : (performanceScore / performanceMetricsCounted);
utilizationScore = (utilizationMetricsCounted == 0) ? utilizationScore : (utilizationScore / utilizationMetricsCounted);
energyScore = (energyMetricsCounted == 0) ? energyScore : (energyScore / energyMetricsCounted);
let res = [];
console.log('Perf', performanceScore, performanceMetricsCounted)
console.log('Util', utilizationScore, utilizationMetricsCounted)
console.log('Energy', energyScore, energyMetricsCounted)
if (performanceScore == 1) {
res.push('<b>Performance:</b> Your job performs well.')
} else if (performanceScore != 0) {
res.push('<b>Performance:</b> Your job performs suboptimal.')
}
if (utilizationScore == 1) {
res.push('<b>Utilization:</b> Your job utilizes resources well.')
} else if (utilizationScore != 0) {
res.push('<b>Utilization:</b> Your job utilizes resources suboptimal.')
}
if (energyScore == 1) {
res.push('<b>Energy:</b> Your job has good energy values.')
} else if (energyScore != 0) {
res.push('<b>Energy:</b> Your job consumes more energy than necessary.')
}
return res;
};
$: summaryMessages = writeSummary(jobFootprintData)
*/
</script> </script>
<Card class="overflow-auto" style="width: {width}; height: {height}"> <Card class="overflow-auto" style="width: {width}; height: {height}">
<TabContent> <!-- on:tab={(e) => (status = e.detail)} --> <TabContent>
{#if showFootprintTab} {#if showFootprintTab}
<TabPane tabId="foot" tab="Footprint" active> <TabPane tabId="foot" tab="Footprint" active>
<CardBody> <!-- Bars CardBody Here-->
{#each jobFootprintData as fpd, index} <JobFootprintBars {job} />
{#if fpd.impact !== 4}
<div class="mb-1 d-flex justify-content-between">
<div>&nbsp;<b>{fpd.name} ({fpd.stat})</b></div>
<div
class="cursor-help d-inline-flex"
id={`footprint-${job.jobId}-${index}`}
>
<div class="mx-1">
{#if fpd.impact === 3}
<Icon name="exclamation-triangle-fill" class="text-danger" />
{:else if fpd.impact === 2}
<Icon name="exclamation-triangle" class="text-warning" />
{:else if fpd.impact === 0}
<Icon name="info-circle" class="text-info" />
{:else if fpd.impact === -1}
<Icon name="info-circle-fill" class="text-danger" />
{/if}
{#if fpd.impact === 3}
<Icon name="emoji-frown" class="text-danger" />
{:else if fpd.impact === 2}
<Icon name="emoji-neutral" class="text-warning" />
{:else if fpd.impact === 1}
<Icon name="emoji-smile" class="text-success" />
{:else if fpd.impact === 0}
<Icon name="emoji-smile" class="text-info" />
{:else if fpd.impact === -1}
<Icon name="emoji-dizzy" class="text-danger" />
{/if}
</div>
<div>
{fpd.value} / {fpd.peak}
{fpd.unit} &nbsp;
</div>
</div>
<Tooltip
target={`footprint-${job.jobId}-${index}`}
placement="right"
>{fpd.message}</Tooltip
>
</div>
<Row cols={12} class="{(jobFootprintData.length == (index + 1)) ? 'mb-0' : 'mb-2'}">
{#if fpd.dir}
<Col xs="1">
<Icon name="caret-left-fill" />
</Col>
{/if}
<Col xs="11" class="align-content-center">
<Progress value={fpd.value} max={fpd.peak} color={fpd.color} />
</Col>
{#if !fpd.dir}
<Col xs="1">
<Icon name="caret-right-fill" />
</Col>
{/if}
</Row>
{:else}
<div class="mb-1 d-flex justify-content-between">
<div>
&nbsp;<b>{fpd.name} ({fpd.stat})</b>
</div>
<div
class="cursor-help d-inline-flex"
id={`footprint-${job.jobId}-${index}`}
>
<div class="mx-1">
<Icon name="info-circle"/>
</div>
<div>
{fpd.value}&nbsp;
</div>
</div>
</div>
<Tooltip
target={`footprint-${job.jobId}-${index}`}
placement="right"
>{fpd.message}</Tooltip
>
{/if}
{/each}
</CardBody>
</TabPane> </TabPane>
{/if} {/if}
<TabPane tabId="polar" tab="Polar" active={!showFootprintTab}> <TabPane tabId="polar" tab="Polar" active={!showFootprintTab}>
<CardBody> <!-- Polar Plot CardBody Here -->
<Polar <JobFootprintPolar {job} />
{polarMetrics}
{jobMetrics}
/>
</CardBody>
</TabPane> </TabPane>
<!--
<TabPane tabId="summary" tab="Summary">
<CardBody>
<p>Based on footprint data, this job performs as follows:</p>
<hr/>
<ul>
{#each summaryMessages as sm}
<li>
{@html sm}
</li>
{/each}
</ul>
</CardBody>
</TabPane>
-->
</TabContent> </TabContent>
</Card> </Card>
<style>
.cursor-help {
cursor: help;
}
</style>

@ -18,6 +18,8 @@
InputGroup, InputGroup,
InputGroupText, InputGroupText,
Icon, Icon,
Row,
Col
} from "@sveltestrap/sveltestrap"; } from "@sveltestrap/sveltestrap";
import { maxScope } from "../generic/utils.js"; import { maxScope } from "../generic/utils.js";
import StatsTableEntry from "./StatsTableEntry.svelte"; import StatsTableEntry from "./StatsTableEntry.svelte";
@ -26,7 +28,7 @@
export let job; export let job;
export let jobMetrics; export let jobMetrics;
const allMetrics = [...new Set(jobMetrics.map((m) => m.name))].sort() const sortedJobMetrics = [...new Set(jobMetrics.map((m) => m.name))].sort()
const scopesForMetric = (metric) => const scopesForMetric = (metric) =>
jobMetrics.filter((jm) => jm.name == metric).map((jm) => jm.scope); jobMetrics.filter((jm) => jm.name == metric).map((jm) => jm.scope);
@ -34,12 +36,13 @@
selectedScopes = {}, selectedScopes = {},
sorting = {}, sorting = {},
isMetricSelectionOpen = false, isMetricSelectionOpen = false,
selectedMetrics = availableMetrics = new Set(),
getContext("cc-config")[ selectedMetrics = (
`job_view_nodestats_selectedMetrics:${job.cluster}` getContext("cc-config")[`job_view_nodestats_selectedMetrics:${job.cluster}:${job.subCluster}`] ||
] || getContext("cc-config")["job_view_nodestats_selectedMetrics"]; getContext("cc-config")[`job_view_nodestats_selectedMetrics:${job.cluster}`]
) || getContext("cc-config")["job_view_nodestats_selectedMetrics"];
for (let metric of allMetrics) { for (let metric of sortedJobMetrics) {
// Not Exclusive or Multi-Node: get maxScope directly (mostly: node) // Not Exclusive or Multi-Node: get maxScope directly (mostly: node)
// -> Else: Load smallest available granularity as default as per availability // -> Else: Load smallest available granularity as default as per availability
const availableScopes = scopesForMetric(metric); const availableScopes = scopesForMetric(metric);
@ -96,15 +99,19 @@
}; };
</script> </script>
<Row>
<Col class="m-2">
<Button outline on:click={() => (isMetricSelectionOpen = true)} class="w-auto px-2" color="primary">
Select Metrics (Selected {selectedMetrics.length} of {availableMetrics.size} available)
</Button>
</Col>
</Row>
<hr class="mb-1 mt-1"/>
<Table class="mb-0"> <Table class="mb-0">
<thead> <thead>
<!-- Header Row 1: Selectors --> <!-- Header Row 1: Selectors -->
<tr> <tr>
<th> <th/>
<Button outline on:click={() => (isMetricSelectionOpen = true)} class="w-100 px-2" color="primary">
Select Metrics
</Button>
</th>
{#each selectedMetrics as metric} {#each selectedMetrics as metric}
<!-- To Match Row-2 Header Field Count--> <!-- To Match Row-2 Header Field Count-->
<th colspan={selectedScopes[metric] == "node" ? 3 : 4}> <th colspan={selectedScopes[metric] == "node" ? 3 : 4}>
@ -163,8 +170,9 @@
<MetricSelection <MetricSelection
cluster={job.cluster} cluster={job.cluster}
subCluster={job.subCluster}
configName="job_view_nodestats_selectedMetrics" configName="job_view_nodestats_selectedMetrics"
allMetrics={new Set(allMetrics)} bind:allMetrics={availableMetrics}
bind:metrics={selectedMetrics} bind:metrics={selectedMetrics}
bind:isOpen={isMetricSelectionOpen} bind:isOpen={isMetricSelectionOpen}
/> />

@ -0,0 +1,210 @@
<!--
@component Job Footprint Bar component; Displays job footprint db data as bars relative to thresholds. Displays quality indicators and tooltips.
Properties:
- `job Object`: The GQL job object
-->
<script>
import { getContext } from "svelte";
import {
CardBody,
Progress,
Icon,
Tooltip,
Row,
Col
} from "@sveltestrap/sveltestrap";
import { findJobFootprintThresholds } from "../../generic/utils.js";
export let job;
// Prepare Job Footprint Data Based On Values Saved In Database
const jobFootprintData = job?.footprint?.map((jf) => {
const fmc = getContext("getMetricConfig")(job.cluster, job.subCluster, jf.name);
if (fmc) {
// Unit
const unit = (fmc?.unit?.prefix ? fmc.unit.prefix : "") + (fmc?.unit?.base ? fmc.unit.base : "")
// Threshold / -Differences
const fmt = findJobFootprintThresholds(job, jf.stat, fmc);
// Define basic data -> Value: Use as Provided
const fmBase = {
name: jf.name,
stat: jf.stat,
value: jf.value,
unit: unit,
peak: fmt.peak,
dir: fmc.lowerIsBetter
};
if (evalFootprint(jf.value, fmt, fmc.lowerIsBetter, "alert")) {
return {
...fmBase,
color: "danger",
message: `Footprint value way ${fmc.lowerIsBetter ? "above" : "below"} expected normal threshold.`,
impact: 3
};
} else if (evalFootprint(jf.value, fmt, fmc.lowerIsBetter, "caution")) {
return {
...fmBase,
color: "warning",
message: `Footprint value ${fmc.lowerIsBetter ? "above" : "below"} expected normal threshold.`,
impact: 2,
};
} else if (evalFootprint(jf.value, fmt, fmc.lowerIsBetter, "normal")) {
return {
...fmBase,
color: "success",
message: "Footprint value within expected thresholds.",
impact: 1,
};
} else if (evalFootprint(jf.value, fmt, fmc.lowerIsBetter, "peak")) {
return {
...fmBase,
color: "info",
message:
"Footprint value above expected normal threshold: Check for artifacts recommended.",
impact: 0,
};
} else {
return {
...fmBase,
color: "secondary",
message:
"Footprint value above expected peak threshold: Check for artifacts!",
impact: -1,
};
}
} else { // No matching metric config: display as single value
return {
name: jf.name,
stat: jf.stat,
value: jf.value,
message:
`No config for metric ${jf.name} found.`,
impact: 4,
};
}
}).sort(function (a, b) { // Sort by impact value primarily, within impact sort name alphabetically
return a.impact - b.impact || ((a.name > b.name) ? 1 : ((b.name > a.name) ? -1 : 0));
});;
function evalFootprint(value, thresholds, lowerIsBetter, level) {
// Handle Metrics in which less value is better
switch (level) {
case "peak":
if (lowerIsBetter)
return false; // metric over peak -> return false to trigger impact -1
else return value <= thresholds.peak && value > thresholds.normal;
case "alert":
if (lowerIsBetter)
return value <= thresholds.peak && value >= thresholds.alert;
else return value <= thresholds.alert && value >= 0;
case "caution":
if (lowerIsBetter)
return value < thresholds.alert && value >= thresholds.caution;
else return value <= thresholds.caution && value > thresholds.alert;
case "normal":
if (lowerIsBetter)
return value < thresholds.caution && value >= 0;
else return value <= thresholds.normal && value > thresholds.caution;
default:
return false;
}
}
</script>
<CardBody>
{#if jobFootprintData.length === 0}
<div class="text-center">No footprint data for job available.</div>
{:else}
{#each jobFootprintData as fpd, index}
{#if fpd.impact !== 4}
<div class="mb-1 d-flex justify-content-between">
<div>&nbsp;<b>{fpd.name} ({fpd.stat})</b></div>
<div
class="cursor-help d-inline-flex"
id={`footprint-${job.jobId}-${index}`}
>
<div class="mx-1">
{#if fpd.impact === 3}
<Icon name="exclamation-triangle-fill" class="text-danger" />
{:else if fpd.impact === 2}
<Icon name="exclamation-triangle" class="text-warning" />
{:else if fpd.impact === 0}
<Icon name="info-circle" class="text-info" />
{:else if fpd.impact === -1}
<Icon name="info-circle-fill" class="text-danger" />
{/if}
{#if fpd.impact === 3}
<Icon name="emoji-frown" class="text-danger" />
{:else if fpd.impact === 2}
<Icon name="emoji-neutral" class="text-warning" />
{:else if fpd.impact === 1}
<Icon name="emoji-smile" class="text-success" />
{:else if fpd.impact === 0}
<Icon name="emoji-smile" class="text-info" />
{:else if fpd.impact === -1}
<Icon name="emoji-dizzy" class="text-danger" />
{/if}
</div>
<div>
{fpd.value} / {fpd.peak}
{fpd.unit} &nbsp;
</div>
</div>
<Tooltip
target={`footprint-${job.jobId}-${index}`}
placement="right"
>{fpd.message}</Tooltip
>
</div>
<Row cols={12} class="{(jobFootprintData.length == (index + 1)) ? 'mb-0' : 'mb-2'}">
{#if fpd.dir}
<Col xs="1">
<Icon name="caret-left-fill" />
</Col>
{/if}
<Col xs="11" class="align-content-center">
<Progress value={fpd.value} max={fpd.peak} color={fpd.color} />
</Col>
{#if !fpd.dir}
<Col xs="1">
<Icon name="caret-right-fill" />
</Col>
{/if}
</Row>
{:else}
<div class="mb-1 d-flex justify-content-between">
<div>
&nbsp;<b>{fpd.name} ({fpd.stat})</b>
</div>
<div
class="cursor-help d-inline-flex"
id={`footprint-${job.jobId}-${index}`}
>
<div class="mx-1">
<Icon name="info-circle"/>
</div>
<div>
{fpd.value}&nbsp;
</div>
</div>
</div>
<Tooltip
target={`footprint-${job.jobId}-${index}`}
placement="right"
>{fpd.message}</Tooltip
>
{/if}
{/each}
{/if}
</CardBody>
<style>
.cursor-help {
cursor: help;
}
</style>

@ -0,0 +1,72 @@
<!--
@component Job Footprint Polar Plot component; Displays queried job metric statistics polar plot.
Properties:
- `job Object`: The GQL job object
-->
<script>
import { getContext } from "svelte";
import {
queryStore,
gql,
getContextClient
} from "@urql/svelte";
import {
Card,
CardBody,
Spinner
} from "@sveltestrap/sveltestrap";
import Polar from "../../generic/plots/Polar.svelte";
import { findJobFootprintThresholds } from "../../generic/utils.js";
export let job;
// Metric Names Configured To Be Footprints For (sub)Cluster
const clusterFootprintMetrics = getContext("clusters")
.find((c) => c.name == job.cluster)?.subClusters
.find((sc) => sc.name == job.subCluster)?.footprint || []
// Get Scaled Peak Threshold Based on Footprint Type ([min, max, avg]) and Job Exclusivity
const polarMetrics = getContext("globalMetrics").reduce((pms, gm) => {
if (clusterFootprintMetrics.includes(gm.name)) {
const fmt = findJobFootprintThresholds(job, gm.footprint, getContext("getMetricConfig")(job.cluster, job.subCluster, gm.name));
pms.push({ name: gm.name, peak: fmt ? fmt.peak : null });
}
return pms;
}, [])
// Pull All Series For Footprint Metrics Statistics Only On Node Scope
const client = getContextClient();
const polarQuery = gql`
query ($dbid: ID!, $selectedMetrics: [String!]!) {
jobMetricStats(id: $dbid, metrics: $selectedMetrics) {
name
stats {
min
avg
max
}
}
}
`;
$: polarData = queryStore({
client: client,
query: polarQuery,
variables:{ dbid: job.id, selectedMetrics: clusterFootprintMetrics },
});
</script>
<CardBody>
{#if $polarData.fetching}
<Spinner />
{:else if $polarData.error}
<Card body color="danger">{$polarData.error.message}</Card>
{:else}
<Polar
{polarMetrics}
polarData={$polarData.data.jobMetricStats}
/>
{/if}
</CardBody>

@ -217,6 +217,7 @@
<tr> <tr>
<td colspan={selectedMetrics.length + 1}> <td colspan={selectedMetrics.length + 1}>
<div style="text-align:center;"> <div style="text-align:center;">
{#if !usePaging}
<p><b> <p><b>
Loading nodes {nodes.length + 1} to Loading nodes {nodes.length + 1} to
{ matchedNodes { matchedNodes
@ -224,6 +225,7 @@
: (nodes.length + paging.itemsPerPage) : (nodes.length + paging.itemsPerPage)
} }
</b></p> </b></p>
{/if}
<Spinner secondary /> <Spinner secondary />
</div> </div>
</td> </td>

@ -102,6 +102,19 @@
Shared Shared
</Button> </Button>
</InputGroup> </InputGroup>
<!-- Fallback -->
{:else if nodeJobsData.jobs.count >= 1}
<InputGroup>
<InputGroupText>
<Icon name="circle-fill"/>
</InputGroupText>
<InputGroupText>
Status
</InputGroupText>
<Button color="success" disabled>
Allocated Jobs
</Button>
</InputGroup>
{:else} {:else}
<InputGroup> <InputGroup>
<InputGroupText> <InputGroupText>

@ -98,12 +98,12 @@
let extendedLegendData = null; let extendedLegendData = null;
$: if ($nodeJobsData?.data) { $: if ($nodeJobsData?.data) {
// Get Shared State of Node: Only Build extended Legend For Shared Nodes // Build Extended for allocated nodes [Commented: Only Build extended Legend For Shared Nodes]
if ($nodeJobsData.data.jobs.count >= 1 && !$nodeJobsData.data.jobs.items[0].exclusive) { if ($nodeJobsData.data.jobs.count >= 1) { // "&& !$nodeJobsData.data.jobs.items[0].exclusive)"
const accSet = Array.from(new Set($nodeJobsData.data.jobs.items const accSet = Array.from(new Set($nodeJobsData.data.jobs.items
.map((i) => i.resources .map((i) => i.resources
.filter((r) => r.hostname === nodeData.host) .filter((r) => (r.hostname === nodeData.host) && r?.accelerators)
.map((r) => r.accelerators) .map((r) => r?.accelerators)
) )
)).flat(2) )).flat(2)