Compare commits

...

7 Commits

Author SHA1 Message Date
allanice001
4d4fda17ca fix: jobs api 2025-09-23 13:40:20 +01:00
allanice001
a59550bd96 feat: remove rollup options & improve clean up 2025-09-23 12:59:15 +01:00
allanice001
182ad023e7 feat: dependencies update 2025-09-23 06:12:51 +01:00
dependabot[bot]
3b32be1a84 Bump github.com/go-viper/mapstructure/v2 from 2.2.1 to 2.4.0 (#32)
Bumps [github.com/go-viper/mapstructure/v2](https://github.com/go-viper/mapstructure) from 2.2.1 to 2.4.0.
- [Release notes](https://github.com/go-viper/mapstructure/releases)
- [Changelog](https://github.com/go-viper/mapstructure/blob/main/CHANGELOG.md)
- [Commits](https://github.com/go-viper/mapstructure/compare/v2.2.1...v2.4.0)

---
updated-dependencies:
- dependency-name: github.com/go-viper/mapstructure/v2
  dependency-version: 2.4.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Alanis <allanice001@users.noreply.github.com>
2025-09-23 06:12:22 +01:00
public-glueops-renovatebot[bot]
8b99c812a6 chore(pin): update @types/react to #patch (#49)
Co-authored-by: public-glueops-renovatebot[bot] <186083205+public-glueops-renovatebot[bot]@users.noreply.github.com>
2025-09-23 06:10:09 +01:00
public-glueops-renovatebot[bot]
bc287a6956 chore(fallback): update axllent/mailpit (#50)
Co-authored-by: public-glueops-renovatebot[bot] <186083205+public-glueops-renovatebot[bot]@users.noreply.github.com>
2025-09-23 06:09:17 +01:00
public-glueops-renovatebot[bot]
0039e81bca feat: update alpine to 3.22 #minor (#68)
Co-authored-by: public-glueops-renovatebot[bot] <186083205+public-glueops-renovatebot[bot]@users.noreply.github.com>
2025-09-23 05:41:28 +01:00
22 changed files with 87 additions and 20142 deletions

View File

@@ -13,13 +13,13 @@ RUN npm i -g yarn pnpm
WORKDIR /src WORKDIR /src
COPY . . COPY . .
RUN make swagger && make build RUN make clean && make swagger && make ui && make build
################################# #################################
# Runtime # Runtime
################################# #################################
FROM alpine:3.20 FROM alpine:3.22@sha256:4bcff63911fcb4448bd4fdacec207030997caf25e9bea4045fa6c8c44de311d1
RUN apk add --no-cache ca-certificates tzdata \ RUN apk add --no-cache ca-certificates tzdata \
&& addgroup -S app && adduser -S app -G app && addgroup -S app && adduser -S app -G app

View File

@@ -3,6 +3,7 @@ GOINSTALL := $(GOCMD) install
BIN ?= autoglue BIN ?= autoglue
MAIN ?= main.go MAIN ?= main.go
UI_DIR ?= ui UI_DIR ?= ui
UI_DEST_DIR ?= internal/ui
SWAG := $(shell command -v swag 2>/dev/null) SWAG := $(shell command -v swag 2>/dev/null)
GMU := $(shell command -v go-mod-upgrade 2>/dev/null) GMU := $(shell command -v go-mod-upgrade 2>/dev/null)
@@ -68,7 +69,7 @@ build: prepare ui swagger
clean: clean:
@echo ">> Cleaning artifacts..." @echo ">> Cleaning artifacts..."
@rm -rf $(BIN) docs/swagger.* docs/docs.go $(UI_DIR)/dist @rm -rf $(BIN) docs/swagger.* docs/docs.go $(UI_DEST_DIR)/dist $(UI_DIR)/dist $(UI_DIR)/node_modules
dev: swagger dev: swagger
@echo ">> Starting Vite (frontend) and Go API (backend)..." @echo ">> Starting Vite (frontend) and Go API (backend)..."

View File

@@ -37,12 +37,12 @@ services:
- postgres:postgres - postgres:postgres
env_file: .env env_file: .env
environment: environment:
DATABASE_URL: postgres://$DB_USER:$DB_PASSWORD@postgres:5432/$DB_NAME PGWEB_DATABASE_URL: postgres://$DB_USER:$DB_PASSWORD@postgres:5432/$DB_NAME
depends_on: depends_on:
- postgres - postgres
mailpit: mailpit:
image: axllent/mailpit@sha256:7b687e9fbc26252866580819733f2dce47edde9b6bf4444da3321fdd06932f02 image: axllent/mailpit@sha256:6abc8e633df15eaf785cfcf38bae48e66f64beecdc03121e249d0f9ec15f0707
restart: always restart: always
ports: ports:
- "1025:1025" - "1025:1025"

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -20,10 +20,8 @@ func NewRouter() http.Handler {
r.Use(cors.Handler(cors.Options{ r.Use(cors.Handler(cors.Options{
AllowedOrigins: []string{ AllowedOrigins: []string{
"http://localhost:5173", "http://*:5173",
"http://127.0.0.1:5173", "http://*:8080",
"http://localhost:8080",
"http://127.0.0.1:8080",
}, },
AllowedMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"}, AllowedMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"},
AllowedHeaders: []string{"Content-Type", "content-type", "Authorization", "authorization", "X-Org-ID", "x-org-id"}, AllowedHeaders: []string{"Content-Type", "content-type", "Authorization", "authorization", "X-Org-ID", "x-org-id"},

View File

@@ -26,6 +26,11 @@ type JobListItem struct {
StartedAt *time.Time `json:"started_at,omitempty"` StartedAt *time.Time `json:"started_at,omitempty"`
UpdatedAt time.Time `json:"updated_at"` UpdatedAt time.Time `json:"updated_at"`
LastError *string `json:"last_error,omitempty"` LastError *string `json:"last_error,omitempty"`
ResultStatus string `json:"result_status"`
Processed int `json:"processed"`
Ready int `json:"ready"`
Failed int `json:"failed"`
ElapsedMs int `json:"elapsed_ms"`
} }
type EnqueueReq struct { type EnqueueReq struct {
@@ -127,7 +132,14 @@ func GetActive(w http.ResponseWriter, r *http.Request) {
var rows []JobListItem var rows []JobListItem
err := db.DB.Model(&models.Job{}). err := db.DB.Model(&models.Job{}).
Select("id, queue_name, status, retry_count, max_retry, scheduled_at, started_at, updated_at, last_error"). Select(`
id, queue_name, status, retry_count, max_retry, scheduled_at, started_at, updated_at, last_error,
COALESCE(result->>'status','') AS result_status,
COALESCE((result->>'processed')::int, 0) AS processed,
COALESCE((result->>'ready')::int, 0) AS ready,
COALESCE((result->>'failed')::int, 0) AS failed,
COALESCE((result->>'elapsed_ms')::int, 0) AS elapsed_ms
`).
Where("status = ?", "running"). Where("status = ?", "running").
Order("started_at DESC NULLS LAST, updated_at DESC"). Order("started_at DESC NULLS LAST, updated_at DESC").
Limit(limit). Limit(limit).
@@ -160,7 +172,14 @@ func GetFailures(w http.ResponseWriter, r *http.Request) {
var rows []JobListItem var rows []JobListItem
err := db.DB.Model(&models.Job{}). err := db.DB.Model(&models.Job{}).
Select("id, queue_name, status, retry_count, max_retry, scheduled_at, started_at, updated_at, last_error"). Select(`
id, queue_name, status, retry_count, max_retry, scheduled_at, started_at, updated_at, last_error,
COALESCE(result->>'status','') AS result_status,
COALESCE((result->>'processed')::int, 0) AS processed,
COALESCE((result->>'ready')::int, 0) AS ready,
COALESCE((result->>'failed')::int, 0) AS failed,
COALESCE((result->>'elapsed_ms')::int, 0) AS elapsed_ms
`).
Where("status = ?", "failed"). Where("status = ?", "failed").
Order("updated_at DESC"). Order("updated_at DESC").
Limit(limit). Limit(limit).

View File

@@ -21,12 +21,14 @@ func LoadKPI(db *gorm.DB) (KPI, error) {
now := time.Now() now := time.Now()
dayAgo := now.Add(-24 * time.Hour) dayAgo := now.Add(-24 * time.Hour)
// Running now
if err := db.Model(&models.Job{}). if err := db.Model(&models.Job{}).
Where("status = ?", "running"). Where("status = ?", "running").
Count(&k.RunningNow).Error; err != nil { Count(&k.RunningNow).Error; err != nil {
return k, err return k, err
} }
// Scheduled in the future
if err := db.Model(&models.Job{}). if err := db.Model(&models.Job{}).
Where("status IN ?", []string{"queued", "scheduled", "pending"}). Where("status IN ?", []string{"queued", "scheduled", "pending"}).
Where("scheduled_at > ?", now). Where("scheduled_at > ?", now).
@@ -34,6 +36,7 @@ func LoadKPI(db *gorm.DB) (KPI, error) {
return k, err return k, err
} }
// Due now (queued/scheduled/pending with scheduled_at <= now)
if err := db.Model(&models.Job{}). if err := db.Model(&models.Job{}).
Where("status IN ?", []string{"queued", "scheduled", "pending"}). Where("status IN ?", []string{"queued", "scheduled", "pending"}).
Where("scheduled_at <= ?", now). Where("scheduled_at <= ?", now).
@@ -41,22 +44,25 @@ func LoadKPI(db *gorm.DB) (KPI, error) {
return k, err return k, err
} }
// Sum of 'ready' over successful jobs in last 24h
if err := db.Model(&models.Job{}). if err := db.Model(&models.Job{}).
Where("status = ?", "success"). Select("COALESCE(SUM((result->>'ready')::int), 0)").
Where("updated_at >= ?", dayAgo). Where("status = 'success' AND updated_at >= ?", dayAgo).
Count(&k.Succeeded24h).Error; err != nil { Scan(&k.Succeeded24h).Error; err != nil {
return k, err return k, err
} }
// Sum of 'failed' over successful jobs in last 24h (failures within a “success” run)
if err := db.Model(&models.Job{}). if err := db.Model(&models.Job{}).
Where("status = ?", "failed"). Select("COALESCE(SUM((result->>'failed')::int), 0)").
Where("updated_at >= ?", dayAgo). Where("status = 'success' AND updated_at >= ?", dayAgo).
Count(&k.Failed24h).Error; err != nil { Scan(&k.Failed24h).Error; err != nil {
return k, err return k, err
} }
// Retryable failed job rows (same as before)
if err := db.Model(&models.Job{}). if err := db.Model(&models.Job{}).
Where("status = ?", "failed"). Where("status = 'failed'").
Where("retry_count < max_retry"). Where("retry_count < max_retry").
Count(&k.Retryable).Error; err != nil { Count(&k.Retryable).Error; err != nil {
return k, err return k, err

View File

@@ -31,11 +31,28 @@ func LoadPerQueue(db *gorm.DB) ([]QueueRollup, error) {
var rr, qd, qf, s24, f24 int64 var rr, qd, qf, s24, f24 int64
var avgDur *float64 var avgDur *float64
_ = db.Model(&models.Job{}).Where("queue_name = ? AND status = 'running'", q).Count(&rr).Error _ = db.Model(&models.Job{}).
_ = db.Model(&models.Job{}).Where("queue_name = ? AND status IN ('queued','scheduled','pending') AND scheduled_at <= ?", q, now).Count(&qd).Error Where("queue_name = ? AND status = 'running'", q).
_ = db.Model(&models.Job{}).Where("queue_name = ? AND status IN ('queued','scheduled','pending') AND scheduled_at > ?", q, now).Count(&qf).Error Count(&rr).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{}).
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
// Sum result.ready / result.failed over successes in last 24h
_ = db.Model(&models.Job{}).
Select("COALESCE(SUM((result->>'ready')::int), 0)").
Where("queue_name = ? AND status = 'success' AND updated_at >= ?", q, dayAgo).
Scan(&s24).Error
_ = db.Model(&models.Job{}).
Select("COALESCE(SUM((result->>'failed')::int), 0)").
Where("queue_name = ? AND status = 'success' AND updated_at >= ?", q, dayAgo).
Scan(&f24).Error
_ = db. _ = db.
Model(&models.Job{}). Model(&models.Job{}).

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,18 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<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-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-B5E6SSBo.js">
<link rel="stylesheet" crossorigin href="/assets/index-CHoyJPs-.css">
</head>
<body>
<div id="root"></div>
</body>
</html>

View File

@@ -14,7 +14,7 @@ import (
// NOTE: Vite outputs to ui/dist with assets in dist/assets. // NOTE: Vite outputs to ui/dist with assets in dist/assets.
// If you add more nested folders in the future, include them here too. // If you add more nested folders in the future, include them here too.
//go:embed dist/* dist/assets/* //go:embed dist
var distFS embed.FS var distFS embed.FS
// spaFileSystem serves embedded dist/ files with SPA fallback to index.html // spaFileSystem serves embedded dist/ files with SPA fallback to index.html
@@ -37,7 +37,8 @@ func (s spaFileSystem) Open(name string) (fs.File, error) {
// BUT only if it's not obviously a static asset extension // BUT only if it's not obviously a static asset extension
ext := strings.ToLower(filepath.Ext(name)) ext := strings.ToLower(filepath.Ext(name))
switch ext { switch ext {
case ".js", ".css", ".map", ".json", ".txt", ".ico", ".png", ".jpg", ".jpeg", ".svg", ".webp", ".gif", ".woff", ".woff2": case ".js", ".css", ".map", ".json", ".txt", ".ico", ".png", ".jpg", ".jpeg",
".svg", ".webp", ".gif", ".woff", ".woff2", ".ttf", ".otf", ".eot", ".wasm":
return nil, fs.ErrNotExist return nil, fs.ErrNotExist
} }
@@ -89,8 +90,14 @@ func SPAHandler() (http.Handler, error) {
w.Header().Set("Cache-Control", "public, max-age=31536000, immutable") w.Header().Set("Cache-Control", "public, max-age=31536000, immutable")
} }
info, _ := f.Stat()
modTime := time.Now()
if info != nil {
modTime = info.ModTime()
}
// Serve content // Serve content
http.ServeContent(w, r, filePath, time.Now(), file{f}) http.ServeContent(w, r, filePath, modTime, file{f})
}), nil }), nil
} }

View File

@@ -2,7 +2,6 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>AutoGlue</title> <title>AutoGlue</title>
</head> </head>

View File

@@ -40,8 +40,8 @@
"@eslint/js": "^9.36.0", "@eslint/js": "^9.36.0",
"@ianvs/prettier-plugin-sort-imports": "^4.7.0", "@ianvs/prettier-plugin-sort-imports": "^4.7.0",
"@types/node": "^24.5.2", "@types/node": "^24.5.2",
"@types/react": "^19.1.13", "@types/react": "19.1.13",
"@types/react-dom": "^19.1.9", "@types/react-dom": "19.1.9",
"@vitejs/plugin-react": "^5.0.3", "@vitejs/plugin-react": "^5.0.3",
"eslint": "^9.36.0", "eslint": "^9.36.0",
"eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-hooks": "^5.2.0",

View File

@@ -34,21 +34,6 @@ export default defineConfig({
chunkSizeWarningLimit: 1000, chunkSizeWarningLimit: 1000,
outDir: "../internal/ui/dist", outDir: "../internal/ui/dist",
emptyOutDir: true, emptyOutDir: true,
rollupOptions: {
output: {
manualChunks(id) {
if (!id.includes("node_modules")) return
if (id.includes("react-router")) return "router"
if (id.includes("@radix-ui")) return "radix"
if (id.includes("lucide-react") || id.includes("react-icons")) return "icons"
if (id.includes("recharts") || id.includes("d3")) return "charts"
if (id.includes("date-fns") || id.includes("dayjs")) return "dates"
return "vendor"
},
},
},
}, },
optimizeDeps: { optimizeDeps: {
include: ["react", "react-dom", "react-router-dom"], include: ["react", "react-dom", "react-router-dom"],

View File

@@ -1503,12 +1503,12 @@
dependencies: dependencies:
undici-types "~7.12.0" undici-types "~7.12.0"
"@types/react-dom@^19.1.9": "@types/react-dom@19.1.9":
version "19.1.9" version "19.1.9"
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-19.1.9.tgz#5ab695fce1e804184767932365ae6569c11b4b4b" resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-19.1.9.tgz#5ab695fce1e804184767932365ae6569c11b4b4b"
integrity sha512-qXRuZaOsAdXKFyOhRBg6Lqqc0yay13vN7KrIg4L7N4aaHN68ma9OK3NE1BoDFgFOTfM7zg+3/8+2n8rLUH3OKQ== integrity sha512-qXRuZaOsAdXKFyOhRBg6Lqqc0yay13vN7KrIg4L7N4aaHN68ma9OK3NE1BoDFgFOTfM7zg+3/8+2n8rLUH3OKQ==
"@types/react@^19.1.13": "@types/react@19.1.13":
version "19.1.13" version "19.1.13"
resolved "https://registry.yarnpkg.com/@types/react/-/react-19.1.13.tgz#fc650ffa680d739a25a530f5d7ebe00cdd771883" resolved "https://registry.yarnpkg.com/@types/react/-/react-19.1.13.tgz#fc650ffa680d739a25a530f5d7ebe00cdd771883"
integrity sha512-hHkbU/eoO3EG5/MZkuFSKmYqPbSVk5byPFa3e7y/8TybHiLMACgI8seVYlicwk7H5K/rI2px9xrQp/C+AUDTiQ== integrity sha512-hHkbU/eoO3EG5/MZkuFSKmYqPbSVk5byPFa3e7y/8TybHiLMACgI8seVYlicwk7H5K/rI2px9xrQp/C+AUDTiQ==