mirror of
https://github.com/GlueOps/autoglue.git
synced 2026-02-13 04:40:05 +01:00
initial jobs dashboard
This commit is contained in:
7
.dockerignore
Normal file
7
.dockerignore
Normal file
@@ -0,0 +1,7 @@
|
||||
postgres
|
||||
.idea
|
||||
docker-compose.yml
|
||||
atlas.hcl
|
||||
autoglue
|
||||
config.yaml
|
||||
.env
|
||||
101
.github/workflows/docker-publish.yml
vendored
Normal file
101
.github/workflows/docker-publish.yml
vendored
Normal file
@@ -0,0 +1,101 @@
|
||||
name: Docker Publish
|
||||
|
||||
# This workflow uses actions that are not certified by GitHub.
|
||||
# They are provided by a third-party and are governed by
|
||||
# separate terms of service, privacy policy, and support
|
||||
# documentation.
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
# Publish semver tags as releases.
|
||||
tags: [ 'v*.*.*' ]
|
||||
pull_request:
|
||||
branches: [ "main" ]
|
||||
|
||||
env:
|
||||
# Use docker.io for Docker Hub if empty
|
||||
REGISTRY: ghcr.io
|
||||
# github.repository as <account>/<repo>
|
||||
IMAGE_NAME: ${{ github.repository }}
|
||||
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
# This is used to complete the identity challenge
|
||||
# with sigstore/fulcio when running outside of PRs.
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
|
||||
# Install the cosign tool except on PR
|
||||
# https://github.com/sigstore/cosign-installer
|
||||
- name: Install cosign
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: sigstore/cosign-installer@d7543c93d881b35a8faa02e8e3605f69b7a1ce62 # v3.10.0
|
||||
with:
|
||||
cosign-release: 'v2.2.4'
|
||||
|
||||
# Set up BuildKit Docker container builder to be able to build
|
||||
# multi-platform images and export cache
|
||||
# https://github.com/docker/setup-buildx-action
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
|
||||
|
||||
# Login against a Docker registry except on PR
|
||||
# https://github.com/docker/login-action
|
||||
- name: Log into registry ${{ env.REGISTRY }}
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# Extract metadata (tags, labels) for Docker
|
||||
# https://github.com/docker/metadata-action
|
||||
- name: Extract Docker metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5.8.0
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
tags: |
|
||||
type=ref,event=tag
|
||||
type=ref,event=branch
|
||||
type=raw,value=latest
|
||||
|
||||
# Build and push Docker image with Buildx (don't push on PR)
|
||||
# https://github.com/docker/build-push-action
|
||||
- name: Build and push Docker image
|
||||
id: build-and-push
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
|
||||
with:
|
||||
context: .
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
platforms: linux/amd64
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
# Sign the resulting Docker image digest except on PRs.
|
||||
# This will only write to the public Rekor transparency log when the Docker
|
||||
# repository is public to avoid leaking data. If you would like to publish
|
||||
# transparency data even for private images, pass --force to cosign below.
|
||||
# https://github.com/sigstore/cosign
|
||||
- name: Sign the published Docker image
|
||||
if: ${{ github.event_name != 'pull_request' }}
|
||||
env:
|
||||
# https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions#using-an-intermediate-environment-variable
|
||||
TAGS: ${{ steps.meta.outputs.tags }}
|
||||
DIGEST: ${{ steps.build-and-push.outputs.digest }}
|
||||
# This step uses the identity token to provision an ephemeral certificate
|
||||
# against the sigstore community Fulcio instance.
|
||||
run: echo "${TAGS}" | xargs -I {} cosign sign --yes {}@${DIGEST}
|
||||
37
Dockerfile
Normal file
37
Dockerfile
Normal file
@@ -0,0 +1,37 @@
|
||||
#################################
|
||||
# Builder: Go + Node in one
|
||||
#################################
|
||||
FROM golang:1.25-alpine AS builder
|
||||
|
||||
RUN apk add --no-cache \
|
||||
git ca-certificates tzdata \
|
||||
build-base \
|
||||
nodejs npm
|
||||
|
||||
RUN npm i -g yarn pnpm
|
||||
|
||||
WORKDIR /src
|
||||
|
||||
COPY . .
|
||||
RUN make swagger && make build
|
||||
|
||||
|
||||
#################################
|
||||
# Runtime
|
||||
#################################
|
||||
FROM alpine:3.20
|
||||
|
||||
RUN apk add --no-cache ca-certificates tzdata \
|
||||
&& addgroup -S app && adduser -S app -G app
|
||||
|
||||
WORKDIR /app
|
||||
COPY --from=builder /src/autoglue /app/autoglue
|
||||
|
||||
ENV PORT=8080
|
||||
EXPOSE 8080
|
||||
USER app
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \
|
||||
CMD wget -qO- "http://127.0.0.1:${PORT}/api/healthz" || exit 1
|
||||
|
||||
ENTRYPOINT ["/app/autoglue"]
|
||||
@@ -30,8 +30,9 @@ var serveCmd = &cobra.Command{
|
||||
Long: "Start the server",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
db.Connect()
|
||||
gdb := db.DB
|
||||
|
||||
jobs, err := bg.NewJobs()
|
||||
jobs, err := bg.NewJobs(gdb)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to init background jobs: %v", err)
|
||||
}
|
||||
@@ -44,23 +45,27 @@ var serveCmd = &cobra.Command{
|
||||
}()
|
||||
defer jobs.Stop()
|
||||
|
||||
// Enqueue one job immediately
|
||||
/*
|
||||
id := uuid.NewString()
|
||||
if _, err := jobs.Enqueue(context.Background(), id, "bootstrap_bastion", bg.BastionBootstrapArgs{}); err != nil {
|
||||
log.Fatalf("[enqueue] failed (job_id=%s): %v", id, err)
|
||||
{
|
||||
// schedule next 03:30 local time
|
||||
now := time.Now()
|
||||
next := time.Date(now.Year(), now.Month(), now.Day(), 3, 30, 0, 0, now.Location())
|
||||
if !next.After(now) {
|
||||
next = next.Add(24 * time.Hour)
|
||||
}
|
||||
log.Printf("[enqueue] queued (job_id=%s)", id)
|
||||
|
||||
// Verify the row exists
|
||||
if got, err := jobs.Client.Get(context.Background(), id); err != nil {
|
||||
log.Fatalf("[verify] Get failed (job_id=%s): %v", id, err)
|
||||
} else if j, ok := got.(*job.Job); ok {
|
||||
log.Printf("[verify] Get ok (job_id=%s, status=%s)", j.ID, j.Status)
|
||||
} else {
|
||||
log.Printf("[verify] Get ok (job_id=%s) but unexpected type %T", id, got)
|
||||
_, err := jobs.Enqueue(
|
||||
context.Background(),
|
||||
uuid.NewString(),
|
||||
"archer_cleanup",
|
||||
bg.CleanupArgs{RetainDays: 7, Table: "jobs"},
|
||||
archer.WithScheduleTime(next),
|
||||
archer.WithMaxRetries(1),
|
||||
)
|
||||
if err != nil {
|
||||
log.Printf("failed to enqueue archer_cleanup: %v", err)
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
// Periodic scheduler
|
||||
schedCtx, schedCancel := context.WithCancel(context.Background())
|
||||
defer schedCancel()
|
||||
|
||||
@@ -1,19 +1,17 @@
|
||||
services:
|
||||
# autoglue:
|
||||
# image: ghcr.io/glueops/autoglue:latest
|
||||
# build:
|
||||
# context: .
|
||||
# dockerfile: Dockerfile
|
||||
# ports:
|
||||
# - 8080:8080
|
||||
# expose:
|
||||
# - 8080
|
||||
# env_file: .env
|
||||
# environment:
|
||||
# AUTOGLUE_DATABASE_DSN: postgres://autoglue:autoglue@postgres:5432/autoglue
|
||||
# AUTOGLUE_BIND_ADDRESS: 0.0.0.0
|
||||
# depends_on:
|
||||
# - postgres
|
||||
autoglue:
|
||||
# image: ghcr.io/glueops/autoglue:latest
|
||||
build: .
|
||||
ports:
|
||||
- 8080:8080
|
||||
expose:
|
||||
- 8080
|
||||
env_file: .env
|
||||
environment:
|
||||
AUTOGLUE_DATABASE_DSN: postgres://$DB_USER:$DB_PASSWORD@postgres:5432/$DB_NAME
|
||||
AUTOGLUE_BIND_ADDRESS: 0.0.0.0
|
||||
depends_on:
|
||||
- postgres
|
||||
|
||||
postgres:
|
||||
build:
|
||||
|
||||
458
docs/docs.go
458
docs/docs.go
@@ -2223,6 +2223,360 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/jobs/active": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Currently running jobs (limit default 100)",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"jobs"
|
||||
],
|
||||
"summary": "Active jobs",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "integer",
|
||||
"default": 100,
|
||||
"description": "Max rows",
|
||||
"name": "limit",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/jobs.JobListItem"
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "unauthorized",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "internal error",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/jobs/enqueue": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Schedules a job on a queue with optional args/schedule",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"jobs"
|
||||
],
|
||||
"summary": "Manually enqueue a job",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "Enqueue request",
|
||||
"name": "payload",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/jobs.EnqueueReq"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"202": {
|
||||
"description": "Accepted",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/jobs.EnqueueResp"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "bad request",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "unauthorized",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "internal error",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/jobs/failures": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Failed jobs ordered by most recent (limit default 100)",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"jobs"
|
||||
],
|
||||
"summary": "Recent failures",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "integer",
|
||||
"default": 100,
|
||||
"description": "Max rows",
|
||||
"name": "limit",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/jobs.JobListItem"
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "unauthorized",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "internal error",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/jobs/kpi": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Aggregated counters across all queues",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"jobs"
|
||||
],
|
||||
"summary": "Jobs KPI",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/jobs.KPI"
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "unauthorized",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "internal error",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/jobs/queues": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Counts and avg duration per queue (last 24h)",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"jobs"
|
||||
],
|
||||
"summary": "Per-queue rollups",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/jobs.QueueRollup"
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "unauthorized",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "internal error",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/jobs/{id}/cancel": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Cancels running or scheduled jobs",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"jobs"
|
||||
],
|
||||
"summary": "Cancel a job",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Job ID",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "no content",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "bad request",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "unauthorized",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "not found",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "internal error",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/jobs/{id}/retry": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Calls Archer ScheduleNow on the job id",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"jobs"
|
||||
],
|
||||
"summary": "Retry a job immediately",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Job ID",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "no content",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "bad request",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "unauthorized",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "not found",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "internal error",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/labels": {
|
||||
"get": {
|
||||
"security": [
|
||||
@@ -6335,6 +6689,110 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"jobs.EnqueueReq": {
|
||||
"type": "object"
|
||||
},
|
||||
"jobs.EnqueueResp": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"jobs.JobListItem": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"last_error": {
|
||||
"type": "string"
|
||||
},
|
||||
"max_retry": {
|
||||
"type": "integer"
|
||||
},
|
||||
"queue_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"retry_count": {
|
||||
"type": "integer"
|
||||
},
|
||||
"scheduled_at": {
|
||||
"type": "string"
|
||||
},
|
||||
"started_at": {
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"type": "string"
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"jobs.KPI": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"dueNow": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"failed24h": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"retryable": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"runningNow": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"scheduledFuture": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"succeeded24h": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
}
|
||||
}
|
||||
},
|
||||
"jobs.QueueRollup": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"avgDurationSecs": {
|
||||
"type": "number",
|
||||
"format": "float64"
|
||||
},
|
||||
"failed24h": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"queueName": {
|
||||
"type": "string"
|
||||
},
|
||||
"queuedDue": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"queuedFuture": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"running": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"success24h": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
}
|
||||
}
|
||||
},
|
||||
"labels.addLabelToPoolRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@@ -2219,6 +2219,360 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/jobs/active": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Currently running jobs (limit default 100)",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"jobs"
|
||||
],
|
||||
"summary": "Active jobs",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "integer",
|
||||
"default": 100,
|
||||
"description": "Max rows",
|
||||
"name": "limit",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/jobs.JobListItem"
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "unauthorized",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "internal error",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/jobs/enqueue": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Schedules a job on a queue with optional args/schedule",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"jobs"
|
||||
],
|
||||
"summary": "Manually enqueue a job",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "Enqueue request",
|
||||
"name": "payload",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/jobs.EnqueueReq"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"202": {
|
||||
"description": "Accepted",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/jobs.EnqueueResp"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "bad request",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "unauthorized",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "internal error",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/jobs/failures": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Failed jobs ordered by most recent (limit default 100)",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"jobs"
|
||||
],
|
||||
"summary": "Recent failures",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "integer",
|
||||
"default": 100,
|
||||
"description": "Max rows",
|
||||
"name": "limit",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/jobs.JobListItem"
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "unauthorized",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "internal error",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/jobs/kpi": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Aggregated counters across all queues",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"jobs"
|
||||
],
|
||||
"summary": "Jobs KPI",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/jobs.KPI"
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "unauthorized",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "internal error",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/jobs/queues": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Counts and avg duration per queue (last 24h)",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"jobs"
|
||||
],
|
||||
"summary": "Per-queue rollups",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/jobs.QueueRollup"
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "unauthorized",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "internal error",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/jobs/{id}/cancel": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Cancels running or scheduled jobs",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"jobs"
|
||||
],
|
||||
"summary": "Cancel a job",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Job ID",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "no content",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "bad request",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "unauthorized",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "not found",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "internal error",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/jobs/{id}/retry": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Calls Archer ScheduleNow on the job id",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"jobs"
|
||||
],
|
||||
"summary": "Retry a job immediately",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Job ID",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "no content",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "bad request",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "unauthorized",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "not found",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "internal error",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/labels": {
|
||||
"get": {
|
||||
"security": [
|
||||
@@ -6331,6 +6685,110 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"jobs.EnqueueReq": {
|
||||
"type": "object"
|
||||
},
|
||||
"jobs.EnqueueResp": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"jobs.JobListItem": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"last_error": {
|
||||
"type": "string"
|
||||
},
|
||||
"max_retry": {
|
||||
"type": "integer"
|
||||
},
|
||||
"queue_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"retry_count": {
|
||||
"type": "integer"
|
||||
},
|
||||
"scheduled_at": {
|
||||
"type": "string"
|
||||
},
|
||||
"started_at": {
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"type": "string"
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"jobs.KPI": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"dueNow": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"failed24h": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"retryable": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"runningNow": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"scheduledFuture": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"succeeded24h": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
}
|
||||
}
|
||||
},
|
||||
"jobs.QueueRollup": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"avgDurationSecs": {
|
||||
"type": "number",
|
||||
"format": "float64"
|
||||
},
|
||||
"failed24h": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"queueName": {
|
||||
"type": "string"
|
||||
},
|
||||
"queuedDue": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"queuedFuture": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"running": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"success24h": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
}
|
||||
}
|
||||
},
|
||||
"labels.addLabelToPoolRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@@ -337,6 +337,78 @@ definitions:
|
||||
status:
|
||||
type: string
|
||||
type: object
|
||||
jobs.EnqueueReq:
|
||||
type: object
|
||||
jobs.EnqueueResp:
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
type: object
|
||||
jobs.JobListItem:
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
last_error:
|
||||
type: string
|
||||
max_retry:
|
||||
type: integer
|
||||
queue_name:
|
||||
type: string
|
||||
retry_count:
|
||||
type: integer
|
||||
scheduled_at:
|
||||
type: string
|
||||
started_at:
|
||||
type: string
|
||||
status:
|
||||
type: string
|
||||
updated_at:
|
||||
type: string
|
||||
type: object
|
||||
jobs.KPI:
|
||||
properties:
|
||||
dueNow:
|
||||
format: int64
|
||||
type: integer
|
||||
failed24h:
|
||||
format: int64
|
||||
type: integer
|
||||
retryable:
|
||||
format: int64
|
||||
type: integer
|
||||
runningNow:
|
||||
format: int64
|
||||
type: integer
|
||||
scheduledFuture:
|
||||
format: int64
|
||||
type: integer
|
||||
succeeded24h:
|
||||
format: int64
|
||||
type: integer
|
||||
type: object
|
||||
jobs.QueueRollup:
|
||||
properties:
|
||||
avgDurationSecs:
|
||||
format: float64
|
||||
type: number
|
||||
failed24h:
|
||||
format: int64
|
||||
type: integer
|
||||
queueName:
|
||||
type: string
|
||||
queuedDue:
|
||||
format: int64
|
||||
type: integer
|
||||
queuedFuture:
|
||||
format: int64
|
||||
type: integer
|
||||
running:
|
||||
format: int64
|
||||
type: integer
|
||||
success24h:
|
||||
format: int64
|
||||
type: integer
|
||||
type: object
|
||||
labels.addLabelToPoolRequest:
|
||||
properties:
|
||||
node_pool_ids:
|
||||
@@ -2191,6 +2263,230 @@ paths:
|
||||
summary: Detach one node pool from a cluster (org scoped)
|
||||
tags:
|
||||
- clusters
|
||||
/api/v1/jobs/{id}/cancel:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Cancels running or scheduled jobs
|
||||
parameters:
|
||||
- description: Job ID
|
||||
in: path
|
||||
name: id
|
||||
required: true
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"204":
|
||||
description: no content
|
||||
schema:
|
||||
type: string
|
||||
"400":
|
||||
description: bad request
|
||||
schema:
|
||||
type: string
|
||||
"401":
|
||||
description: unauthorized
|
||||
schema:
|
||||
type: string
|
||||
"404":
|
||||
description: not found
|
||||
schema:
|
||||
type: string
|
||||
"500":
|
||||
description: internal error
|
||||
schema:
|
||||
type: string
|
||||
security:
|
||||
- BearerAuth: []
|
||||
summary: Cancel a job
|
||||
tags:
|
||||
- jobs
|
||||
/api/v1/jobs/{id}/retry:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Calls Archer ScheduleNow on the job id
|
||||
parameters:
|
||||
- description: Job ID
|
||||
in: path
|
||||
name: id
|
||||
required: true
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"204":
|
||||
description: no content
|
||||
schema:
|
||||
type: string
|
||||
"400":
|
||||
description: bad request
|
||||
schema:
|
||||
type: string
|
||||
"401":
|
||||
description: unauthorized
|
||||
schema:
|
||||
type: string
|
||||
"404":
|
||||
description: not found
|
||||
schema:
|
||||
type: string
|
||||
"500":
|
||||
description: internal error
|
||||
schema:
|
||||
type: string
|
||||
security:
|
||||
- BearerAuth: []
|
||||
summary: Retry a job immediately
|
||||
tags:
|
||||
- jobs
|
||||
/api/v1/jobs/active:
|
||||
get:
|
||||
description: Currently running jobs (limit default 100)
|
||||
parameters:
|
||||
- default: 100
|
||||
description: Max rows
|
||||
in: query
|
||||
name: limit
|
||||
type: integer
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
items:
|
||||
$ref: '#/definitions/jobs.JobListItem'
|
||||
type: array
|
||||
"401":
|
||||
description: unauthorized
|
||||
schema:
|
||||
type: string
|
||||
"500":
|
||||
description: internal error
|
||||
schema:
|
||||
type: string
|
||||
security:
|
||||
- BearerAuth: []
|
||||
summary: Active jobs
|
||||
tags:
|
||||
- jobs
|
||||
/api/v1/jobs/enqueue:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Schedules a job on a queue with optional args/schedule
|
||||
parameters:
|
||||
- description: Enqueue request
|
||||
in: body
|
||||
name: payload
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/jobs.EnqueueReq'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"202":
|
||||
description: Accepted
|
||||
schema:
|
||||
$ref: '#/definitions/jobs.EnqueueResp'
|
||||
"400":
|
||||
description: bad request
|
||||
schema:
|
||||
type: string
|
||||
"401":
|
||||
description: unauthorized
|
||||
schema:
|
||||
type: string
|
||||
"500":
|
||||
description: internal error
|
||||
schema:
|
||||
type: string
|
||||
security:
|
||||
- BearerAuth: []
|
||||
summary: Manually enqueue a job
|
||||
tags:
|
||||
- jobs
|
||||
/api/v1/jobs/failures:
|
||||
get:
|
||||
description: Failed jobs ordered by most recent (limit default 100)
|
||||
parameters:
|
||||
- default: 100
|
||||
description: Max rows
|
||||
in: query
|
||||
name: limit
|
||||
type: integer
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
items:
|
||||
$ref: '#/definitions/jobs.JobListItem'
|
||||
type: array
|
||||
"401":
|
||||
description: unauthorized
|
||||
schema:
|
||||
type: string
|
||||
"500":
|
||||
description: internal error
|
||||
schema:
|
||||
type: string
|
||||
security:
|
||||
- BearerAuth: []
|
||||
summary: Recent failures
|
||||
tags:
|
||||
- jobs
|
||||
/api/v1/jobs/kpi:
|
||||
get:
|
||||
description: Aggregated counters across all queues
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/jobs.KPI'
|
||||
"401":
|
||||
description: unauthorized
|
||||
schema:
|
||||
type: string
|
||||
"500":
|
||||
description: internal error
|
||||
schema:
|
||||
type: string
|
||||
security:
|
||||
- BearerAuth: []
|
||||
summary: Jobs KPI
|
||||
tags:
|
||||
- jobs
|
||||
/api/v1/jobs/queues:
|
||||
get:
|
||||
description: Counts and avg duration per queue (last 24h)
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
items:
|
||||
$ref: '#/definitions/jobs.QueueRollup'
|
||||
type: array
|
||||
"401":
|
||||
description: unauthorized
|
||||
schema:
|
||||
type: string
|
||||
"500":
|
||||
description: internal error
|
||||
schema:
|
||||
type: string
|
||||
security:
|
||||
- BearerAuth: []
|
||||
summary: Per-queue rollups
|
||||
tags:
|
||||
- jobs
|
||||
/api/v1/labels:
|
||||
get:
|
||||
consumes:
|
||||
|
||||
35
go.mod
35
go.mod
@@ -11,29 +11,29 @@ require (
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/joho/godotenv v1.5.1
|
||||
github.com/spf13/cobra v1.10.1
|
||||
github.com/spf13/viper v1.20.1
|
||||
github.com/spf13/viper v1.21.0
|
||||
github.com/swaggo/http-swagger/v2 v2.0.2
|
||||
github.com/swaggo/swag v1.16.6
|
||||
github.com/wneessen/go-mail v0.6.2
|
||||
golang.org/x/crypto v0.41.0
|
||||
golang.org/x/text v0.28.0
|
||||
github.com/wneessen/go-mail v0.7.0
|
||||
golang.org/x/crypto v0.42.0
|
||||
golang.org/x/text v0.29.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
gorm.io/datatypes v1.2.6
|
||||
gorm.io/datatypes v1.2.7
|
||||
gorm.io/driver/postgres v1.6.0
|
||||
gorm.io/gorm v1.30.2
|
||||
gorm.io/gorm v1.31.0
|
||||
)
|
||||
|
||||
require (
|
||||
filippo.io/edwards25519 v1.1.0 // indirect
|
||||
github.com/KyleBanks/depth v1.2.1 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/fsnotify/fsnotify v1.8.0 // indirect
|
||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.19.5 // indirect
|
||||
github.com/go-openapi/jsonreference v0.20.0 // indirect
|
||||
github.com/go-openapi/spec v0.20.6 // indirect
|
||||
github.com/go-openapi/swag v0.19.15 // indirect
|
||||
github.com/go-sql-driver/mysql v1.8.1 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
||||
github.com/goccy/go-json v0.10.5 // indirect
|
||||
github.com/google/go-cmp v0.7.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
@@ -46,21 +46,20 @@ require (
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/lib/pq v1.10.9 // indirect
|
||||
github.com/mailru/easyjson v0.7.6 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/rogpeppe/go-internal v1.13.1 // indirect
|
||||
github.com/sagikazarmark/locafero v0.7.0 // indirect
|
||||
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||
github.com/spf13/afero v1.12.0 // indirect
|
||||
github.com/spf13/cast v1.7.1 // indirect
|
||||
github.com/spf13/pflag v1.0.9 // indirect
|
||||
github.com/sagikazarmark/locafero v0.11.0 // indirect
|
||||
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
|
||||
github.com/spf13/afero v1.15.0 // indirect
|
||||
github.com/spf13/cast v1.10.0 // indirect
|
||||
github.com/spf13/pflag v1.0.10 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
github.com/swaggo/files/v2 v2.0.0 // indirect
|
||||
go.uber.org/atomic v1.9.0 // indirect
|
||||
go.uber.org/multierr v1.9.0 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/mod v0.27.0 // indirect
|
||||
golang.org/x/sync v0.16.0 // indirect
|
||||
golang.org/x/sys v0.35.0 // indirect
|
||||
golang.org/x/sync v0.17.0 // indirect
|
||||
golang.org/x/sys v0.36.0 // indirect
|
||||
golang.org/x/tools v0.36.0 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gorm.io/driver/mysql v1.5.6 // indirect
|
||||
|
||||
149
go.sum
149
go.sum
@@ -16,8 +16,8 @@ github.com/earthboundkid/versioninfo/v2 v2.24.1 h1:SJTMHaoUx3GzjjnUO1QzP3ZXK6Ee/
|
||||
github.com/earthboundkid/versioninfo/v2 v2.24.1/go.mod h1:VcWEooDEuyUJnMfbdTh0uFN4cfEIg+kHMuWB2CDCLjw=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
|
||||
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE=
|
||||
github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
|
||||
github.com/go-chi/cors v1.2.2 h1:Jmey33TE+b+rB7fT8MUy1u0I4L+NARQlK6LhzKPSyQE=
|
||||
@@ -35,8 +35,8 @@ github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/
|
||||
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
||||
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-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.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
|
||||
@@ -45,7 +45,6 @@ github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0kt
|
||||
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
|
||||
github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
|
||||
github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
@@ -81,41 +80,42 @@ github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN
|
||||
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA=
|
||||
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOjvxI=
|
||||
github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||
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/microsoft/go-mssqldb v1.7.2 h1:CHkFJiObW7ItKTJfHo1QX7QBBD1iV+mn1eOyRP3b/PA=
|
||||
github.com/microsoft/go-mssqldb v1.7.2/go.mod h1:kOvZKUdrhhFQmxLZqbwUV0rHkNkZpthMITIb2Ko1IoA=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
|
||||
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
||||
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo=
|
||||
github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k=
|
||||
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
|
||||
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
|
||||
github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs=
|
||||
github.com/spf13/afero v1.12.0/go.mod h1:ZTlWwG4/ahT8W7T0WQ5uYmjI9duaLQGy3Q2OAl4sk/4=
|
||||
github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
|
||||
github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||
github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc=
|
||||
github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=
|
||||
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw=
|
||||
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U=
|
||||
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
|
||||
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
|
||||
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
|
||||
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
|
||||
github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
|
||||
github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=
|
||||
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
|
||||
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4=
|
||||
github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4=
|
||||
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
|
||||
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
|
||||
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||
github.com/swaggo/files/v2 v2.0.0 h1:hmAt8Dkynw7Ssz46F6pn8ok6YmGZqHSVLZ+HQM7i0kw=
|
||||
@@ -124,89 +124,24 @@ github.com/swaggo/http-swagger/v2 v2.0.2 h1:FKCdLsl+sFCx60KFsyM0rDarwiUSZ8DqbfSy
|
||||
github.com/swaggo/http-swagger/v2 v2.0.2/go.mod h1:r7/GBkAWIfK6E/OLnE8fXnviHiDeAHmgIyooa4xm3AQ=
|
||||
github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI=
|
||||
github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg=
|
||||
github.com/wneessen/go-mail v0.6.2 h1:c6V7c8D2mz868z9WJ+8zDKtUyLfZ1++uAZmo2GRFji8=
|
||||
github.com/wneessen/go-mail v0.6.2/go.mod h1:L/PYjPK3/2ZlNb2/FjEBIn9n1rUWjW+Toy531oVmeb4=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
|
||||
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
|
||||
go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
|
||||
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.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.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
|
||||
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
|
||||
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
|
||||
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.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
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=
|
||||
github.com/wneessen/go-mail v0.7.0 h1:/Wmgd5AVjp5PA+Ken5EFfr+QR83gmqHli9HcAhh0vnU=
|
||||
github.com/wneessen/go-mail v0.7.0/go.mod h1:+TkW6QP3EVkgTEqHtVmnAE/1MRhmzb8Y9/W3pweuS+k=
|
||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
|
||||
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
|
||||
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
|
||||
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/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-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
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.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
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.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
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.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
||||
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
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-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/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.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.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
||||
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
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-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.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.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
|
||||
golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4=
|
||||
golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw=
|
||||
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.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.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.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
|
||||
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
|
||||
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.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.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
||||
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
|
||||
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ=
|
||||
golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA=
|
||||
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
|
||||
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
|
||||
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
|
||||
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
@@ -219,16 +154,16 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C
|
||||
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gorm.io/datatypes v1.2.6 h1:KafLdXvFUhzNeL2ncm03Gl3eTLONQfNKZ+wJ+9Y4Nck=
|
||||
gorm.io/datatypes v1.2.6/go.mod h1:M2iO+6S3hhi4nAyYe444Pcb0dcIiOMJ7QHaUXxyiNZY=
|
||||
gorm.io/datatypes v1.2.7 h1:ww9GAhF1aGXZY3EB3cJPJ7//JiuQo7DlQA7NNlVaTdk=
|
||||
gorm.io/datatypes v1.2.7/go.mod h1:M2iO+6S3hhi4nAyYe444Pcb0dcIiOMJ7QHaUXxyiNZY=
|
||||
gorm.io/driver/mysql v1.5.6 h1:Ld4mkIickM+EliaQZQx3uOJDJHtrd70MxAUqWqlx3Y8=
|
||||
gorm.io/driver/mysql v1.5.6/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM=
|
||||
gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4=
|
||||
gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo=
|
||||
gorm.io/driver/sqlite v1.4.3 h1:HBBcZSDnWi5BW3B3rwvVTc510KGkBkexlOg0QrmLUuU=
|
||||
gorm.io/driver/sqlite v1.4.3/go.mod h1:0Aq3iPO+v9ZKbcdiz8gLWRw5VOPcBOPUQJFLq5e2ecI=
|
||||
gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ=
|
||||
gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8=
|
||||
gorm.io/driver/sqlserver v1.6.0 h1:VZOBQVsVhkHU/NzNhRJKoANt5pZGQAS1Bwc6m6dgfnc=
|
||||
gorm.io/driver/sqlserver v1.6.0/go.mod h1:WQzt4IJo/WHKnckU9jXBLMJIVNMVeTu25dnOzehntWw=
|
||||
gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
|
||||
gorm.io/gorm v1.30.2 h1:f7bevlVoVe4Byu3pmbWPVHnPsLoWaMjEb7/clyr9Ivs=
|
||||
gorm.io/gorm v1.30.2/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=
|
||||
gorm.io/gorm v1.31.0 h1:0VlycGreVhK7RF/Bwt51Fk8v0xLiiiFdbGDPIZQ7mJY=
|
||||
gorm.io/gorm v1.31.0/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"github.com/glueops/autoglue/internal/handlers/authn"
|
||||
"github.com/glueops/autoglue/internal/handlers/clusters"
|
||||
"github.com/glueops/autoglue/internal/handlers/health"
|
||||
"github.com/glueops/autoglue/internal/handlers/jobs"
|
||||
"github.com/glueops/autoglue/internal/handlers/labels"
|
||||
"github.com/glueops/autoglue/internal/handlers/nodepools"
|
||||
"github.com/glueops/autoglue/internal/handlers/orgs"
|
||||
@@ -36,6 +37,17 @@ func RegisterRoutes(r chi.Router) {
|
||||
ad.Delete("/users/{userId}", authn.AdminDeleteUser)
|
||||
})
|
||||
|
||||
v1.Route("/jobs", func(j chi.Router) {
|
||||
j.Use(authMW)
|
||||
j.Get("/kpi", jobs.GetKPI)
|
||||
j.Get("/queues", jobs.GetQueues)
|
||||
j.Get("/active", jobs.GetActive)
|
||||
j.Get("/failures", jobs.GetFailures)
|
||||
j.Post("/{id}/retry", jobs.RetryNow)
|
||||
j.Post("/{id}/cancel", jobs.Cancel)
|
||||
j.Post("/{id}/enqueue", jobs.Enqueue)
|
||||
})
|
||||
|
||||
v1.Route("/auth", func(a chi.Router) {
|
||||
a.Post("/login", authn.Login)
|
||||
a.Post("/register", authn.Register)
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// internal/bg/bg.go
|
||||
package bg
|
||||
|
||||
import (
|
||||
@@ -11,10 +10,13 @@ import (
|
||||
|
||||
"github.com/dyaksa/archer"
|
||||
"github.com/spf13/viper"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type Jobs struct{ Client *archer.Client }
|
||||
|
||||
var BgJobs *Jobs
|
||||
|
||||
func archerOptionsFromDSN(dsn string) (*archer.Options, error) {
|
||||
u, err := url.Parse(dsn)
|
||||
if err != nil {
|
||||
@@ -40,7 +42,7 @@ func archerOptionsFromDSN(dsn string) (*archer.Options, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
func NewJobs() (*Jobs, error) {
|
||||
func NewJobs(gdb *gorm.DB) (*Jobs, error) {
|
||||
opts, err := archerOptionsFromDSN(viper.GetString("database.dsn"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -54,6 +56,10 @@ func NewJobs() (*Jobs, error) {
|
||||
if timeoutSec <= 0 {
|
||||
timeoutSec = 60
|
||||
}
|
||||
retainDays := viper.GetInt("archer.cleanup_retain_days")
|
||||
if retainDays <= 0 {
|
||||
retainDays = 7
|
||||
}
|
||||
|
||||
// LOG what we’re connecting to (sanitized) so you can confirm DB/host
|
||||
log.Printf("[archer] addr=%s db=%s user=%s ssl=%s", opts.Addr, opts.DBName, opts.User, opts.SSL)
|
||||
@@ -74,7 +80,15 @@ func NewJobs() (*Jobs, error) {
|
||||
archer.WithTimeout(time.Duration(timeoutSec)*time.Second),
|
||||
)
|
||||
|
||||
return &Jobs{Client: c}, nil
|
||||
jobs := &Jobs{Client: c}
|
||||
|
||||
c.Register(
|
||||
"archer_cleanup",
|
||||
CleanupWorker(gdb, jobs, retainDays),
|
||||
archer.WithInstances(1),
|
||||
archer.WithTimeout(5*time.Minute),
|
||||
)
|
||||
return jobs, nil
|
||||
}
|
||||
|
||||
func (j *Jobs) Start() error { return j.Client.Start() }
|
||||
|
||||
53
internal/bg/cleanup.go
Normal file
53
internal/bg/cleanup.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package bg
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/dyaksa/archer"
|
||||
"github.com/dyaksa/archer/job"
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type CleanupArgs struct {
|
||||
RetainDays int `json:"retain_days"`
|
||||
Table string `json:"table"`
|
||||
}
|
||||
|
||||
type JobRow struct {
|
||||
ID string `gorm:"primaryKey"`
|
||||
Status string
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
func (JobRow) TableName() string { return "jobs" }
|
||||
|
||||
func CleanupWorker(gdb *gorm.DB, jobs *Jobs, retainDays int) archer.WorkerFn {
|
||||
return func(ctx context.Context, j job.Job) (any, error) {
|
||||
if err := CleanupJobs(gdb, retainDays); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// schedule tomorrow 03:30
|
||||
next := time.Now().Truncate(24 * time.Hour).Add(24 * time.Hour).Add(3*time.Hour + 30*time.Minute)
|
||||
|
||||
_, _ = jobs.Enqueue(
|
||||
ctx,
|
||||
uuid.NewString(),
|
||||
"archer_cleanup",
|
||||
CleanupArgs{RetainDays: retainDays, Table: "jobs"},
|
||||
archer.WithScheduleTime(next),
|
||||
archer.WithMaxRetries(1),
|
||||
)
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
|
||||
func CleanupJobs(db *gorm.DB, retainDays int) error {
|
||||
cutoff := time.Now().AddDate(0, 0, -retainDays)
|
||||
return db.
|
||||
Where("status IN ?", []string{"success", "failed", "cancelled"}).
|
||||
Where("updated_at < ?", cutoff).
|
||||
Delete(&JobRow{}).Error
|
||||
}
|
||||
293
internal/handlers/jobs/jobs.go
Normal file
293
internal/handlers/jobs/jobs.go
Normal file
@@ -0,0 +1,293 @@
|
||||
package jobs
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/dyaksa/archer"
|
||||
"github.com/glueops/autoglue/internal/bg"
|
||||
"github.com/glueops/autoglue/internal/db"
|
||||
"github.com/glueops/autoglue/internal/db/models"
|
||||
"github.com/glueops/autoglue/internal/middleware"
|
||||
"github.com/glueops/autoglue/internal/response"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type JobListItem struct {
|
||||
ID string `json:"id"`
|
||||
QueueName string `json:"queue_name"`
|
||||
Status string `json:"status"`
|
||||
RetryCount int `json:"retry_count"`
|
||||
MaxRetry int `json:"max_retry"`
|
||||
ScheduledAt time.Time `json:"scheduled_at"`
|
||||
StartedAt *time.Time `json:"started_at,omitempty"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
LastError *string `json:"last_error,omitempty"`
|
||||
}
|
||||
|
||||
type EnqueueReq struct {
|
||||
Queue string `json:"queue"`
|
||||
Args json.RawMessage `json:"args"` // keep raw and pass through to Archer
|
||||
MaxRetries *int `json:"max_retries,omitempty"`
|
||||
ScheduleAt *time.Time `json:"schedule_at,omitempty"`
|
||||
}
|
||||
|
||||
type EnqueueResp struct {
|
||||
ID string `json:"id"`
|
||||
}
|
||||
|
||||
func parseLimit(r *http.Request, def int) int {
|
||||
if s := r.URL.Query().Get("limit"); s != "" {
|
||||
if n, err := strconv.Atoi(s); err == nil && n > 0 && n <= 1000 {
|
||||
return n
|
||||
}
|
||||
}
|
||||
return def
|
||||
}
|
||||
|
||||
func isNotFoundErr(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
msg := err.Error()
|
||||
return msg == "job not found" || msg == "no rows in result set"
|
||||
}
|
||||
|
||||
// ---------------------- READ ENDPOINTS ----------------------
|
||||
|
||||
// GetKPI godoc
|
||||
// @Summary Jobs KPI
|
||||
// @Description Aggregated counters across all queues
|
||||
// @Tags jobs
|
||||
// @Security BearerAuth
|
||||
// @Produce json
|
||||
// @Success 200 {object} jobs.KPI
|
||||
// @Failure 401 {string} string "unauthorized"
|
||||
// @Failure 500 {string} string "internal error"
|
||||
// @Router /api/v1/jobs/kpi [get]
|
||||
func GetKPI(w http.ResponseWriter, r *http.Request) {
|
||||
if middleware.GetAuthContext(r) == nil {
|
||||
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
k, err := LoadKPI(db.DB)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
_ = response.JSON(w, http.StatusOK, k)
|
||||
}
|
||||
|
||||
// GetQueues godoc
|
||||
// @Summary Per-queue rollups
|
||||
// @Description Counts and avg duration per queue (last 24h)
|
||||
// @Tags jobs
|
||||
// @Security BearerAuth
|
||||
// @Produce json
|
||||
// @Success 200 {array} jobs.QueueRollup
|
||||
// @Failure 401 {string} string "unauthorized"
|
||||
// @Failure 500 {string} string "internal error"
|
||||
// @Router /api/v1/jobs/queues [get]
|
||||
func GetQueues(w http.ResponseWriter, r *http.Request) {
|
||||
if middleware.GetAuthContext(r) == nil {
|
||||
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
rows, err := LoadPerQueue(db.DB)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
_ = response.JSON(w, http.StatusOK, rows)
|
||||
}
|
||||
|
||||
// GetActive godoc
|
||||
// @Summary Active jobs
|
||||
// @Description Currently running jobs (limit default 100)
|
||||
// @Tags jobs
|
||||
// @Security BearerAuth
|
||||
// @Produce json
|
||||
// @Param limit query int false "Max rows" default(100)
|
||||
// @Success 200 {array} jobs.JobListItem
|
||||
// @Failure 401 {string} string "unauthorized"
|
||||
// @Failure 500 {string} string "internal error"
|
||||
// @Router /api/v1/jobs/active [get]
|
||||
func GetActive(w http.ResponseWriter, r *http.Request) {
|
||||
if middleware.GetAuthContext(r) == nil {
|
||||
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
limit := parseLimit(r, 100)
|
||||
|
||||
var rows []JobListItem
|
||||
err := db.DB.Model(&models.Job{}).
|
||||
Select("id, queue_name, status, retry_count, max_retry, scheduled_at, started_at, updated_at, last_error").
|
||||
Where("status = ?", "running").
|
||||
Order("started_at DESC NULLS LAST, updated_at DESC").
|
||||
Limit(limit).
|
||||
Scan(&rows).Error
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
_ = response.JSON(w, http.StatusOK, rows)
|
||||
}
|
||||
|
||||
// GetFailures godoc
|
||||
// @Summary Recent failures
|
||||
// @Description Failed jobs ordered by most recent (limit default 100)
|
||||
// @Tags jobs
|
||||
// @Security BearerAuth
|
||||
// @Produce json
|
||||
// @Param limit query int false "Max rows" default(100)
|
||||
// @Success 200 {array} jobs.JobListItem
|
||||
// @Failure 401 {string} string "unauthorized"
|
||||
// @Failure 500 {string} string "internal error"
|
||||
// @Router /api/v1/jobs/failures [get]
|
||||
func GetFailures(w http.ResponseWriter, r *http.Request) {
|
||||
if middleware.GetAuthContext(r) == nil {
|
||||
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
limit := parseLimit(r, 100)
|
||||
|
||||
var rows []JobListItem
|
||||
err := db.DB.Model(&models.Job{}).
|
||||
Select("id, queue_name, status, retry_count, max_retry, scheduled_at, started_at, updated_at, last_error").
|
||||
Where("status = ?", "failed").
|
||||
Order("updated_at DESC").
|
||||
Limit(limit).
|
||||
Scan(&rows).Error
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
_ = response.JSON(w, http.StatusOK, rows)
|
||||
}
|
||||
|
||||
// ---------------------- MUTATION ENDPOINTS ----------------------
|
||||
|
||||
// RetryNow godoc
|
||||
// @Summary Retry a job immediately
|
||||
// @Description Calls Archer ScheduleNow on the job id
|
||||
// @Tags jobs
|
||||
// @Security BearerAuth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path string true "Job ID"
|
||||
// @Success 204 {string} string "no content"
|
||||
// @Failure 400 {string} string "bad request"
|
||||
// @Failure 401 {string} string "unauthorized"
|
||||
// @Failure 404 {string} string "not found"
|
||||
// @Failure 500 {string} string "internal error"
|
||||
// @Router /api/v1/jobs/{id}/retry [post]
|
||||
func RetryNow(w http.ResponseWriter, r *http.Request) {
|
||||
if middleware.GetAuthContext(r) == nil {
|
||||
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
id := chi.URLParam(r, "id")
|
||||
if id == "" {
|
||||
http.Error(w, "missing id", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
// archer.ScheduleNow returns (any, error); if the id is unknown, expect an error you can surface as 404
|
||||
if _, err := bg.BgJobs.Client.ScheduleNow(r.Context(), id); err != nil {
|
||||
status := http.StatusInternalServerError
|
||||
// (Optional) map error text if Archer returns a recognizable "not found"
|
||||
if isNotFoundErr(err) {
|
||||
status = http.StatusNotFound
|
||||
}
|
||||
http.Error(w, err.Error(), status)
|
||||
return
|
||||
}
|
||||
response.NoContent(w)
|
||||
}
|
||||
|
||||
// Cancel godoc
|
||||
// @Summary Cancel a job
|
||||
// @Description Cancels running or scheduled jobs
|
||||
// @Tags jobs
|
||||
// @Security BearerAuth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path string true "Job ID"
|
||||
// @Success 204 {string} string "no content"
|
||||
// @Failure 400 {string} string "bad request"
|
||||
// @Failure 401 {string} string "unauthorized"
|
||||
// @Failure 404 {string} string "not found"
|
||||
// @Failure 500 {string} string "internal error"
|
||||
// @Router /api/v1/jobs/{id}/cancel [post]
|
||||
func Cancel(w http.ResponseWriter, r *http.Request) {
|
||||
if middleware.GetAuthContext(r) == nil {
|
||||
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
id := chi.URLParam(r, "id")
|
||||
if id == "" {
|
||||
http.Error(w, "missing id", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if _, err := bg.BgJobs.Client.Cancel(r.Context(), id); err != nil {
|
||||
status := http.StatusInternalServerError
|
||||
if isNotFoundErr(err) {
|
||||
status = http.StatusNotFound
|
||||
}
|
||||
http.Error(w, err.Error(), status)
|
||||
return
|
||||
}
|
||||
response.NoContent(w)
|
||||
}
|
||||
|
||||
// Enqueue godoc
|
||||
// @Summary Manually enqueue a job
|
||||
// @Description Schedules a job on a queue with optional args/schedule
|
||||
// @Tags jobs
|
||||
// @Security BearerAuth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param payload body jobs.EnqueueReq true "Enqueue request"
|
||||
// @Success 202 {object} jobs.EnqueueResp
|
||||
// @Failure 400 {string} string "bad request"
|
||||
// @Failure 401 {string} string "unauthorized"
|
||||
// @Failure 500 {string} string "internal error"
|
||||
// @Router /api/v1/jobs/enqueue [post]
|
||||
func Enqueue(w http.ResponseWriter, r *http.Request) {
|
||||
if middleware.GetAuthContext(r) == nil {
|
||||
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
var req EnqueueReq
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "invalid json: "+err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if req.Queue == "" {
|
||||
http.Error(w, "queue is required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
id := uuid.NewString()
|
||||
opts := []archer.FnOptions{}
|
||||
if req.MaxRetries != nil {
|
||||
opts = append(opts, archer.WithMaxRetries(*req.MaxRetries))
|
||||
}
|
||||
if req.ScheduleAt != nil {
|
||||
opts = append(opts, archer.WithScheduleTime(*req.ScheduleAt))
|
||||
}
|
||||
|
||||
if _, err := bg.BgJobs.Client.Schedule(r.Context(), id, req.Queue, req.Args, opts...); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
_ = response.JSON(w, http.StatusAccepted, EnqueueResp{ID: id})
|
||||
}
|
||||
66
internal/handlers/jobs/kpi.go
Normal file
66
internal/handlers/jobs/kpi.go
Normal file
@@ -0,0 +1,66 @@
|
||||
package jobs
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/glueops/autoglue/internal/db/models"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type KPI struct {
|
||||
RunningNow int64
|
||||
DueNow int64
|
||||
ScheduledFuture int64
|
||||
Succeeded24h int64
|
||||
Failed24h int64
|
||||
Retryable int64
|
||||
}
|
||||
|
||||
func LoadKPI(db *gorm.DB) (KPI, error) {
|
||||
var k KPI
|
||||
now := time.Now()
|
||||
dayAgo := now.Add(-24 * time.Hour)
|
||||
|
||||
if err := db.Model(&models.Job{}).
|
||||
Where("status = ?", "running").
|
||||
Count(&k.RunningNow).Error; err != nil {
|
||||
return k, err
|
||||
}
|
||||
|
||||
if err := db.Model(&models.Job{}).
|
||||
Where("status IN ?", []string{"queued", "scheduled", "pending"}).
|
||||
Where("scheduled_at > ?", now).
|
||||
Count(&k.ScheduledFuture).Error; err != nil {
|
||||
return k, err
|
||||
}
|
||||
|
||||
if err := db.Model(&models.Job{}).
|
||||
Where("status IN ?", []string{"queued", "scheduled", "pending"}).
|
||||
Where("scheduled_at <= ?", now).
|
||||
Count(&k.DueNow).Error; err != nil {
|
||||
return k, err
|
||||
}
|
||||
|
||||
if err := db.Model(&models.Job{}).
|
||||
Where("status = ?", "success").
|
||||
Where("updated_at >= ?", dayAgo).
|
||||
Count(&k.Succeeded24h).Error; err != nil {
|
||||
return k, err
|
||||
}
|
||||
|
||||
if err := db.Model(&models.Job{}).
|
||||
Where("status = ?", "failed").
|
||||
Where("updated_at >= ?", dayAgo).
|
||||
Count(&k.Failed24h).Error; err != nil {
|
||||
return k, err
|
||||
}
|
||||
|
||||
if err := db.Model(&models.Job{}).
|
||||
Where("status = ?", "failed").
|
||||
Where("retry_count < max_retry").
|
||||
Count(&k.Retryable).Error; err != nil {
|
||||
return k, err
|
||||
}
|
||||
|
||||
return k, nil
|
||||
}
|
||||
64
internal/handlers/jobs/queue.go
Normal file
64
internal/handlers/jobs/queue.go
Normal file
@@ -0,0 +1,64 @@
|
||||
package jobs
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/glueops/autoglue/internal/db/models"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type QueueRollup struct {
|
||||
QueueName string
|
||||
Running int64
|
||||
QueuedDue int64
|
||||
QueuedFuture int64
|
||||
Success24h int64
|
||||
Failed24h int64
|
||||
AvgDurationSecs float64
|
||||
}
|
||||
|
||||
func LoadPerQueue(db *gorm.DB) ([]QueueRollup, error) {
|
||||
var queues []string
|
||||
if err := db.Model(&models.Job{}).Distinct().Pluck("queue_name", &queues).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
dayAgo := now.Add(-24 * time.Hour)
|
||||
out := make([]QueueRollup, 0, len(queues))
|
||||
|
||||
for _, q := range queues {
|
||||
var rr, qd, qf, s24, f24 int64
|
||||
var avgDur *float64
|
||||
|
||||
_ = db.Model(&models.Job{}).Where("queue_name = ? AND status = 'running'", q).Count(&rr).Error
|
||||
_ = db.Model(&models.Job{}).Where("queue_name = ? AND status IN ('queued','scheduled','pending') AND scheduled_at <= ?", q, now).Count(&qd).Error
|
||||
_ = db.Model(&models.Job{}).Where("queue_name = ? AND status IN ('queued','scheduled','pending') AND scheduled_at > ?", q, now).Count(&qf).Error
|
||||
_ = db.Model(&models.Job{}).Where("queue_name = ? AND status = 'success' AND updated_at >= ?", q, dayAgo).Count(&s24).Error
|
||||
_ = db.Model(&models.Job{}).Where("queue_name = ? AND status = 'failed' AND updated_at >= ?", q, dayAgo).Count(&f24).Error
|
||||
|
||||
_ = db.
|
||||
Model(&models.Job{}).
|
||||
Select("AVG(EXTRACT(EPOCH FROM (updated_at - started_at)))").
|
||||
Where("queue_name = ? AND status = 'success' AND started_at IS NOT NULL AND updated_at >= ?", q, dayAgo).
|
||||
Scan(&avgDur).Error
|
||||
|
||||
out = append(out, QueueRollup{
|
||||
QueueName: q,
|
||||
Running: rr,
|
||||
QueuedDue: qd,
|
||||
QueuedFuture: qf,
|
||||
Success24h: s24,
|
||||
Failed24h: f24,
|
||||
AvgDurationSecs: coalesceF64(avgDur, 0),
|
||||
})
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func coalesceF64(p *float64, d float64) float64 {
|
||||
if p == nil {
|
||||
return d
|
||||
}
|
||||
return *p
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
1
internal/ui/dist/assets/index--a4aJrTK.css
vendored
1
internal/ui/dist/assets/index--a4aJrTK.css
vendored
File diff suppressed because one or more lines are too long
1
internal/ui/dist/assets/index-CHoyJPs-.css
vendored
Normal file
1
internal/ui/dist/assets/index-CHoyJPs-.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
internal/ui/dist/assets/index-CmZFDWt2.js
vendored
1
internal/ui/dist/assets/index-CmZFDWt2.js
vendored
File diff suppressed because one or more lines are too long
1
internal/ui/dist/assets/index-DSxuk_EI.js
vendored
Normal file
1
internal/ui/dist/assets/index-DSxuk_EI.js
vendored
Normal file
File diff suppressed because one or more lines are too long
6
internal/ui/dist/index.html
vendored
6
internal/ui/dist/index.html
vendored
@@ -5,12 +5,12 @@
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>AutoGlue</title>
|
||||
<script type="module" crossorigin src="/assets/index-CmZFDWt2.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index-DSxuk_EI.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="/assets/router-CANfZtzM.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/vendor-DvippHRz.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/radix-DRmH1vcw.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/icons-DQ1I1M7X.js">
|
||||
<link rel="stylesheet" crossorigin href="/assets/index--a4aJrTK.css">
|
||||
<link rel="modulepreload" crossorigin href="/assets/icons-B5E6SSBo.js">
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-CHoyJPs-.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -21,6 +21,7 @@ import { NotFoundPage } from "@/pages/error/not-found.tsx"
|
||||
import { SshKeysPage } from "@/pages/security/ssh.tsx"
|
||||
import { MemberManagement } from "@/pages/settings/members.tsx"
|
||||
import { OrgManagement } from "@/pages/settings/orgs.tsx"
|
||||
import JobsDashboard from "@/pages/settings/jobs.tsx";
|
||||
|
||||
function App() {
|
||||
return (
|
||||
@@ -57,6 +58,7 @@ function App() {
|
||||
</Route>
|
||||
|
||||
<Route path="/settings">
|
||||
<Route path="jobs" element={<JobsDashboard />} />
|
||||
<Route path="orgs" element={<OrgManagement />} />
|
||||
<Route path="members" element={<MemberManagement />} />
|
||||
<Route path="me" element={<Me />} />
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
UsersIcon,
|
||||
} from "lucide-react"
|
||||
import { AiOutlineCluster } from "react-icons/ai"
|
||||
import {GrUserWorker} from "react-icons/gr";
|
||||
|
||||
export type NavItem = {
|
||||
label: string
|
||||
@@ -95,6 +96,11 @@ export const items = [
|
||||
label: "Settings",
|
||||
icon: SettingsIcon,
|
||||
items: [
|
||||
{
|
||||
label: "Jobs",
|
||||
icon: GrUserWorker,
|
||||
to: '/settings/jobs',
|
||||
},
|
||||
{
|
||||
label: "Organizations",
|
||||
to: "/settings/orgs",
|
||||
|
||||
@@ -74,8 +74,8 @@ const CreateClusterSchema = z.object({
|
||||
name: z.string().trim().min(2, "Name is too short"),
|
||||
provider: z.string().trim().min(2, "Provider is too short"),
|
||||
region: z.string().trim().min(1, "Region is required"),
|
||||
node_pool_ids: z.array(z.string().uuid()).optional().default([]),
|
||||
bastion_server_id: z.string().uuid().optional(),
|
||||
node_pool_ids: z.array(z.uuid()).default([]).optional(),
|
||||
bastion_server_id: z.uuid().optional(),
|
||||
cluster_load_balancer: z.string().optional(),
|
||||
control_load_balancer: z.string().optional(),
|
||||
kubeconfig: z.string().optional(),
|
||||
@@ -88,7 +88,7 @@ const UpdateClusterSchema = z
|
||||
provider: z.string().trim().min(2, "Provider is too short").optional(),
|
||||
region: z.string().trim().min(1, "Region is required").optional(),
|
||||
status: z.string().trim().min(1, "Status is required").optional(),
|
||||
bastion_server_id: z.string().uuid().or(z.literal("")).optional(),
|
||||
bastion_server_id: z.uuid().or(z.literal("")).optional(),
|
||||
cluster_load_balancer: z.string().optional(),
|
||||
control_load_balancer: z.string().optional(),
|
||||
kubeconfig: z.string().optional(),
|
||||
@@ -113,7 +113,7 @@ const AttachPoolsSchema = z.object({
|
||||
export type AttachPoolsValues = z.infer<typeof AttachPoolsSchema>
|
||||
|
||||
const SetBastionSchema = z.object({
|
||||
server_id: z.string().uuid({ message: "Enter a valid Server UUID" }),
|
||||
server_id: z.uuid({ message: "Enter a valid Server UUID" }),
|
||||
})
|
||||
export type SetBastionValues = z.infer<typeof SetBastionSchema>
|
||||
|
||||
|
||||
189
ui/src/pages/settings/jobs.tsx
Normal file
189
ui/src/pages/settings/jobs.tsx
Normal file
@@ -0,0 +1,189 @@
|
||||
import {useCallback, useEffect, useMemo, useState} from "react";
|
||||
import {api} from "@/lib/api.ts";
|
||||
import {Card, CardContent, CardHeader, CardTitle} from "@/components/ui/card.tsx";
|
||||
import {Table, TableBody, TableCell, TableHead, TableHeader, TableRow} from "@/components/ui/table.tsx";
|
||||
|
||||
export interface KPI {
|
||||
RunningNow: number;
|
||||
DueNow: number;
|
||||
ScheduledFuture: number;
|
||||
Succeeded24h: number;
|
||||
Failed24h: number;
|
||||
Retryable: number;
|
||||
}
|
||||
|
||||
export interface QueueRollup {
|
||||
QueueName: string;
|
||||
Running: number;
|
||||
QueuedDue: number;
|
||||
QueuedFuture: number;
|
||||
Success24h: number;
|
||||
Failed24h: number;
|
||||
AvgDurationSecs: number;
|
||||
}
|
||||
|
||||
|
||||
export interface JobListItem {
|
||||
id: string;
|
||||
queue_name: string;
|
||||
status: string;
|
||||
retry_count: number;
|
||||
max_retry: number;
|
||||
scheduled_at: string; // ISO
|
||||
started_at?: string; // ISO
|
||||
updated_at: string; // ISO
|
||||
last_error?: string;
|
||||
}
|
||||
|
||||
|
||||
const fmtNumber = (n: number | undefined) => (n ?? 0).toLocaleString();
|
||||
|
||||
|
||||
const fmtSeconds = (secs: number) => {
|
||||
if (!isFinite(secs) || secs <= 0) return "–";
|
||||
if (secs < 60) return `${secs.toFixed(0)}s`;
|
||||
if (secs < 3600) return `${Math.floor(secs / 60)}m ${Math.floor(secs % 60)}s`;
|
||||
const h = Math.floor(secs / 3600);
|
||||
const m = Math.floor((secs % 3600) / 60);
|
||||
return `${h}h ${m}m`;
|
||||
};
|
||||
|
||||
export default function JobsDashboard() {
|
||||
const [kpi, setKpi] = useState<KPI | null>(null);
|
||||
const [queues, setQueues] = useState<QueueRollup[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [autoRefresh, setAutoRefresh] = useState(true);
|
||||
const [refreshMs, setRefreshMs] = useState(5000);
|
||||
|
||||
const loadAll = useCallback(async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const [k, q] = await Promise.all([
|
||||
api.get<KPI>("/api/v1/jobs/kpi"),
|
||||
api.get<QueueRollup[]>("/api/v1/jobs/queues"),
|
||||
])
|
||||
setKpi(k)
|
||||
setQueues(q)
|
||||
} catch (e: any) {
|
||||
setError(e.message || String(e));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
}, [])
|
||||
|
||||
useEffect(() => { void loadAll(); }, [loadAll]);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (!autoRefresh) return;
|
||||
const id = setInterval(loadAll, refreshMs);
|
||||
return () => clearInterval(id);
|
||||
}, [autoRefresh, refreshMs, loadAll]);
|
||||
|
||||
const totals = useMemo(() => ({
|
||||
queues: queues.length,
|
||||
running: queues.reduce((s, q) => s + q.Running, 0),
|
||||
due: queues.reduce((s, q) => s + q.QueuedDue, 0),
|
||||
future: queues.reduce((s, q) => s + q.QueuedFuture, 0),
|
||||
}), [queues]);
|
||||
|
||||
|
||||
return (
|
||||
<div className="p-6 space-y-6">
|
||||
<header className="flex items-center justify-between gap-3">
|
||||
<h1 className="text-2xl font-semibold tracking-tight">Jobs Dashboard</h1>
|
||||
<div className="flex items-center gap-3">
|
||||
<label className="flex items-center gap-2 text-sm">
|
||||
<input type="checkbox" className="h-4 w-4" checked={autoRefresh} onChange={(e)=>setAutoRefresh(e.target.checked)} />
|
||||
Auto refresh
|
||||
</label>
|
||||
<select
|
||||
className="border rounded px-2 py-1 text-sm"
|
||||
value={refreshMs}
|
||||
onChange={(e)=>setRefreshMs(parseInt(e.target.value))}
|
||||
>
|
||||
<option value={3000}>3s</option>
|
||||
<option value={5000}>5s</option>
|
||||
<option value={10000}>10s</option>
|
||||
<option value={30000}>30s</option>
|
||||
</select>
|
||||
|
||||
<button
|
||||
className="px-3 py-1.5 rounded bg-slate-900 text-white text-sm hover:opacity-90"
|
||||
onClick={loadAll}
|
||||
disabled={loading}
|
||||
>{loading ? "Refreshing…" : "Refresh"}</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{error && (
|
||||
<div className="rounded border border-red-300 bg-red-50 text-red-800 p-3 text-sm">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* KPI cards */}
|
||||
<section className="grid gap-4 grid-cols-1 sm:grid-cols-2 lg:grid-cols-6">
|
||||
<KpiCard label="Running" value={fmtNumber(kpi?.RunningNow)} />
|
||||
<KpiCard label="Due now" value={fmtNumber(kpi?.DueNow)} />
|
||||
<KpiCard label="Scheduled" value={fmtNumber(kpi?.ScheduledFuture)} />
|
||||
<KpiCard label="Succeeded (24h)" value={fmtNumber(kpi?.Succeeded24h)} />
|
||||
<KpiCard label="Failed (24h)" value={fmtNumber(kpi?.Failed24h)} />
|
||||
<KpiCard label="Retryable" value={fmtNumber(kpi?.Retryable)} />
|
||||
</section>
|
||||
|
||||
{/* Per-queue table */}
|
||||
<section className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-lg font-medium">Queues <span className="text-slate-500 text-sm">({totals.queues})</span></h2>
|
||||
</div>
|
||||
<div className="overflow-x-auto rounded border">
|
||||
<Table className="min-w-full text-sm">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Queue</TableHead>
|
||||
<TableHead className='text-right'>Running</TableHead>
|
||||
<TableHead className='text-right'>Due</TableHead>
|
||||
<TableHead className='text-right'>Future</TableHead>
|
||||
<TableHead className='text-right'>Success 24h</TableHead>
|
||||
<TableHead className='text-right'>Failed 24h</TableHead>
|
||||
<TableHead className='text-right'>Avg Duration</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{queues.map((q) => (
|
||||
<TableRow key={q.QueueName} className="border-t">
|
||||
<TableCell>{q.QueueName}</TableCell>
|
||||
<TableCell className='text-right'>{q.Running}</TableCell>
|
||||
<TableCell className='text-right'>{q.QueuedDue}</TableCell>
|
||||
<TableCell className='text-right'>{q.QueuedFuture}</TableCell>
|
||||
<TableCell className='text-right'>{q.Success24h}</TableCell>
|
||||
<TableCell className='text-right'>{q.Failed24h}</TableCell>
|
||||
<TableCell className='text-right'>{fmtSeconds(q.AvgDurationSecs)}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* <ManualEnqueue onSubmitted={loadAll} /> */}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function KpiCard({ label, value }: { label: string; value: string }) {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{label}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="mt-1 text-2xl font-semibold">{value}</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user