diff --git a/Makefile b/Makefile index 6e0031d..49fb9ba 100644 --- a/Makefile +++ b/Makefile @@ -1,19 +1,25 @@ -.PHONY: fmt lint test mocks test_coverage +default: help + +help: ## list makefile targets + @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' + + +.PHONY: help fmt lint test mocks test_coverage GO_PKGS := $(shell go list -f {{.Dir}} ./...) -fmt: +fmt: ## gofmt all files @go list -f {{.Dir}} ./... | xargs -I{} gofmt -w -s {} -lint: +lint: ## run golangci-lint @golangci-lint run -test: +test: ## run tests @go test -race -v $(GO_FLAGS) -count=1 $(GO_PKGS) -test_coverage: +test_coverage: ## run tests with coverage @go test -race -v $(GO_FLAGS) -count=1 -coverprofile=coverage.out -covermode=atomic $(GO_PKGS) @go tool cover -html coverage.out -mocks: +mocks: ## generate mocks @go generate ./... diff --git a/README.md b/README.md index 6e7b725..43d834a 100644 --- a/README.md +++ b/README.md @@ -121,7 +121,7 @@ Connect to `ws://localhost:8080/ws` for real-time job updates. A full-featured example demonstrating 14 different job types is available in the [examples](./examples/) directory: ```bash -cd examples +cd examples/getting_started go run main.go ``` @@ -140,6 +140,9 @@ go run main.go Visit `http://localhost:8080` to see the UI in action. +### Basic Auth +See the [basic_auth](./examples/basic_auth) example for how to secure GoCron-UI using a simple basic auth middleware with the credentials being sourced via environment variables. + ## Deployment ### Binary Distribution diff --git a/examples/basic_auth/README.md b/examples/basic_auth/README.md new file mode 100644 index 0000000..ef2d7f1 --- /dev/null +++ b/examples/basic_auth/README.md @@ -0,0 +1,116 @@ +# GoCronUI Basic Auth Example + +This is a simple example demonstrating securing gocron-ui with basic auth: + +```go +> GOCRON_UI_USERNAME=admin GOCRON_UI_PASSWORD=password go run main.go +``` + +In another terminal: + +**valid credentials** +```bash +curl -v -u "admin:password" 127.0.0.1:8080/api/jobs | jq . +\ % Total % Received % Xferd Average Speed Time Time Time Current + Dload Upload Total Spent Left Speed + 0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0* Trying 127.0.0.1:8080... +* Connected to 127.0.0.1 (127.0.0.1) port 8080 +* Server auth using Basic with user 'admin' +> GET /api/jobs HTTP/1.1 +> Host: 127.0.0.1:8080 +> Authorization: Basic YWRtaW46cGFzc3dvcmQ= +> User-Agent: curl/8.7.1 +> Accept: */* +> +* Request completely sent off +< HTTP/1.1 200 OK +< Content-Type: application/json +< Vary: Origin +< Date: Sun, 09 Nov 2025 23:09:13 GMT +< Content-Length: 1187 +< +{ [1187 bytes data] +100 1187 100 1187 0 0 1837k 0 --:--:-- --:--:-- --:--:-- 1159k +* Connection #0 to host 127.0.0.1 left intact +[ + { + "id": "3681db0c-fd30-48ed-af21-2082f6a9f3d7", + "name": "simple-10s-interval", + "tags": [ + "interval", + "simple" + ], + "nextRun": "2025-11-10T10:09:07+11:00", + "lastRun": "2025-11-10T10:09:07+11:00", + "nextRuns": [ + "2025-11-10T10:09:07+11:00", + "2025-11-10T10:09:17+11:00", + "2025-11-10T10:09:27+11:00", + "2025-11-10T10:09:37+11:00", + "2025-11-10T10:09:47+11:00" + ], + "schedule": "Every 10 seconds", + "scheduleDetail": "Duration: 10s" + }, + { + "id": "7ff3624c-248e-47f6-90bd-49da5451ddcf", + "name": "simple-20s-interval", + "tags": [ + "interval", + "simple" + ], + "nextRun": "2025-11-10T10:09:07+11:00", + "lastRun": "2025-11-10T10:09:07+11:00", + "nextRuns": [ + "2025-11-10T10:09:07+11:00", + "2025-11-10T10:09:27+11:00", + "2025-11-10T10:09:47+11:00", + "2025-11-10T10:10:07+11:00", + "2025-11-10T10:10:27+11:00" + ], + "schedule": "Every 20 seconds", + "scheduleDetail": "Duration: 20s" + }, + { + "id": "ad6ddb39-e1a3-4dc4-8539-cf1d9a4667eb", + "name": "simple-5s-interval", + "tags": [ + "interval", + "simple" + ], + "nextRun": "2025-11-10T10:09:17+11:00", + "lastRun": "2025-11-10T10:09:12+11:00", + "nextRuns": [ + "2025-11-10T10:09:17+11:00", + "2025-11-10T10:09:22+11:00", + "2025-11-10T10:09:27+11:00", + "2025-11-10T10:09:32+11:00", + "2025-11-10T10:09:37+11:00" + ], + "schedule": "Every 5 seconds", + "scheduleDetail": "Duration: 5s" + } +] +``` + +**missing or invalid credentials** +```bash +curl -v 127.0.0.1:8080/api/jobs +* Trying 127.0.0.1:8080... +* Connected to 127.0.0.1 (127.0.0.1) port 8080 +> GET /api/jobs HTTP/1.1 +> Host: 127.0.0.1:8080 +> User-Agent: curl/8.7.1 +> Accept: */* +> +* Request completely sent off +< HTTP/1.1 401 Unauthorized +< Content-Type: text/plain; charset=utf-8 +< Www-Authenticate: Basic realm="restricted", charset="UTF-8" +< X-Content-Type-Options: nosniff +< Date: Sun, 09 Nov 2025 23:10:02 GMT +< Content-Length: 13 +< +Unauthorized +* Connection #0 to host 127.0.0.1 left intact +``` \ No newline at end of file diff --git a/examples/basic_auth/main.go b/examples/basic_auth/main.go new file mode 100644 index 0000000..ce31158 --- /dev/null +++ b/examples/basic_auth/main.go @@ -0,0 +1,129 @@ +package main + +import ( + "crypto/sha256" + "crypto/subtle" + "flag" + "fmt" + "log" + "net/http" + "os" + "os/signal" + "strings" + "syscall" + "time" + + "github.com/go-co-op/gocron-ui/server" + "github.com/go-co-op/gocron/v2" +) + +const ( + usernameEnv = "GOCRON_UI_USERNAME" + passwordEnv = "GOCRON_UI_PASSWORD" +) + +var jobs = []struct { + name string + definition gocron.JobDefinition + task gocron.Task + options []gocron.JobOption +}{ + { + "simple-10s-interval", gocron.DurationJob(10 * time.Second), gocron.NewTask(func() { log.Println("Running 10-second interval job") }), []gocron.JobOption{gocron.WithName("simple-10s-interval"), gocron.WithTags("interval", "simple")}, + }, + { + "simple-5s-interval", gocron.DurationJob(5 * time.Second), gocron.NewTask(func() { log.Println("Running 5-second interval job") }), []gocron.JobOption{gocron.WithName("simple-5s-interval"), gocron.WithTags("interval", "simple")}, + }, + { + "simple-20s-interval", gocron.DurationJob(20 * time.Second), gocron.NewTask(func() { log.Println("Running 20-second interval job") }), []gocron.JobOption{gocron.WithName("simple-20s-interval"), gocron.WithTags("interval", "simple")}, + }, +} + +func main() { + username, usernameOK := os.LookupEnv(usernameEnv) + password, passwordOK := os.LookupEnv(passwordEnv) + + if (!usernameOK || !passwordOK) || username == "" || password == "" { + log.Fatalf("Environment variables %s and %s must be set for basic authentication", usernameEnv, passwordEnv) + } + + port := flag.Int("port", 8080, "Port to run the server on") + title := flag.String("title", "GoCron Scheduler", "Custom title for the UI") + flag.Parse() + + // create the gocron scheduler + scheduler, err := gocron.NewScheduler() + if err != nil { + log.Fatalf("Failed to create scheduler: %v", err) + } + + // add jobs to the scheduler + for _, job := range jobs { + if _, err := scheduler.NewJob(job.definition, job.task, job.options...); err != nil { + log.Printf("Error creating job: %v", err) + } + } + + // start the scheduler + scheduler.Start() + log.Println("Scheduler started with", len(scheduler.Jobs()), "jobs") + + // create and start the API server with custom title + srv := server.NewServer(scheduler, *port, server.WithTitle(*title)) + + // start server in a goroutine + go func() { + addr := fmt.Sprintf(":%d", *port) + log.Println("\n" + strings.Repeat("=", 70)) + log.Printf("GoCron UI Server Started with Basic Authentication") + log.Println(strings.Repeat("=", 70)) + log.Printf("Web UI: http://localhost%s", addr) + log.Printf("API: http://localhost%s/api", addr) + log.Printf("WebSocket: ws://localhost%s/ws", addr) + log.Printf("Total Jobs: %d", len(scheduler.Jobs())) + log.Println(strings.Repeat("=", 70) + "\n") + + if err := http.ListenAndServe(addr, basicAuthMiddleware(srv.Router, username, password)); err != nil { + log.Fatalf("Server failed to start: %v", err) + } + }() + + // wait for interrupt signal to gracefully shutdown + quit := make(chan os.Signal, 1) + signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) + <-quit + + log.Println("\nShutting down server...") + + // shutdown scheduler + if err := scheduler.Shutdown(); err != nil { + log.Printf("Error shutting down scheduler: %v", err) + } + + log.Println("Server stopped gracefully") +} + +// https://www.alexedwards.net/blog/basic-authentication-in-go +func basicAuthMiddleware(next http.Handler, expectedUsername, expectedPassword string) http.HandlerFunc { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + username, password, ok := r.BasicAuth() + if ok { + usernameHash := sha256.Sum256([]byte(username)) + passwordHash := sha256.Sum256([]byte(password)) + + expectedUsernameHash := sha256.Sum256([]byte(expectedUsername)) + expectedPasswordHash := sha256.Sum256([]byte(expectedPassword)) + + usernameMatch := (subtle.ConstantTimeCompare(usernameHash[:], expectedUsernameHash[:]) == 1) + passwordMatch := (subtle.ConstantTimeCompare(passwordHash[:], expectedPasswordHash[:]) == 1) + + if usernameMatch && passwordMatch { + next.ServeHTTP(w, r) + return + } + } + + w.Header().Set("WWW-Authenticate", `Basic realm="restricted", charset="UTF-8"`) + http.Error(w, "Unauthorized", http.StatusUnauthorized) + }) +} diff --git a/examples/README.md b/examples/getting_started/README.md similarity index 100% rename from examples/README.md rename to examples/getting_started/README.md diff --git a/examples/getting_started/main.go b/examples/getting_started/main.go new file mode 100644 index 0000000..2680de5 --- /dev/null +++ b/examples/getting_started/main.go @@ -0,0 +1,183 @@ +package main + +import ( + "context" + "flag" + "fmt" + "log" + "math/rand" + "net/http" + "os" + "os/signal" + "strings" + "syscall" + "time" + + "github.com/go-co-op/gocron-ui/server" + "github.com/go-co-op/gocron/v2" + "github.com/google/uuid" +) + +var jobs = []struct { + definition gocron.JobDefinition + task gocron.Task + options []gocron.JobOption +}{ + { + gocron.DurationJob(10 * time.Second), + gocron.NewTask(func() { log.Println("Running 10-second interval job") }), + []gocron.JobOption{gocron.WithName("simple-10s-interval"), gocron.WithTags("interval", "simple")}, + }, + { + gocron.DurationJob(5 * time.Second), + gocron.NewTask(func() { log.Println("Running 5-second interval job") }), + []gocron.JobOption{gocron.WithName("simple-5s-interval"), gocron.WithTags("interval", "simple")}, + }, + + { + gocron.CronJob("* * * * *", false), + gocron.NewTask(func() { log.Println("Cron job executed (every minute)") }), + []gocron.JobOption{gocron.WithName("cron-every-minute"), gocron.WithTags("cron", "periodic")}, + }, + { + gocron.DailyJob(1, gocron.NewAtTimes(gocron.NewAtTime(14, 30, 0))), + gocron.NewTask(func() { log.Println("Daily job executed at 2:30 PM") }), + []gocron.JobOption{gocron.WithName("daily-afternoon-report"), gocron.WithTags("daily", "report")}, + }, + { + gocron.WeeklyJob(1, gocron.NewWeekdays(time.Monday, time.Wednesday, time.Friday), gocron.NewAtTimes(gocron.NewAtTime(9, 0, 0))), + gocron.NewTask(func() { log.Println("Weekly job executed (Mon, Wed, Fri at 9:00 AM)") }), + []gocron.JobOption{gocron.WithName("weekly-mwf-morning"), gocron.WithTags("weekly", "morning", "report")}, + }, + { + gocron.DurationJob(12 * time.Second), + gocron.NewTask(func(name string, count int) { log.Printf("Job with parameters: name=%s, count=%d", name, count) }, "example-job", 42), + []gocron.JobOption{gocron.WithName("parameterized-job"), gocron.WithTags("parameters", "demo")}, + }, + { + gocron.DurationJob(8 * time.Second), + gocron.NewTask(func(ctx context.Context) { log.Printf("Job with context executed, context: %v", ctx) }), + []gocron.JobOption{gocron.WithName("context-aware-job"), gocron.WithTags("context", "advanced")}, + }, + { + gocron.DurationRandomJob(5*time.Second, 15*time.Second), + gocron.NewTask(func() { log.Println("Random interval job executed (5-15 seconds)") }), + []gocron.JobOption{gocron.WithName("random-interval-job"), gocron.WithTags("random", "variable")}, + }, + { + gocron.DurationJob(5 * time.Second), + gocron.NewTask(func() { + log.Println("Singleton job started") + time.Sleep(8 * time.Second) + log.Println("Singleton job completed") + }), + []gocron.JobOption{gocron.WithName("singleton-mode-job"), gocron.WithTags("singleton", "long-running"), gocron.WithSingletonMode(gocron.LimitModeReschedule)}, + }, + { + gocron.DurationJob(7 * time.Second), + gocron.NewTask(func() { log.Println("Limited run job executed") }), + []gocron.JobOption{gocron.WithName("limited-run-job"), gocron.WithTags("limited", "demo"), gocron.WithLimitedRuns(3)}, + }, + { + gocron.DurationJob(15 * time.Second), + gocron.NewTask(func() { + log.Println("Job with listeners executed") + time.Sleep(time.Duration(rand.Intn(3)+1) * time.Second) + }), + []gocron.JobOption{ + gocron.WithName("event-listener-job"), + gocron.WithTags("events", "monitoring"), + gocron.WithEventListeners( + gocron.AfterJobRuns(func(_ uuid.UUID, jobName string) { + log.Printf(" → AfterJobRuns: %s completed", jobName) + }), + gocron.BeforeJobRuns(func(_ uuid.UUID, jobName string) { + log.Printf(" → BeforeJobRuns: %s starting", jobName) + }), + ), + }, + }, + { + gocron.OneTimeJob(gocron.OneTimeJobStartDateTime(time.Now().Add(30 * time.Second))), + gocron.NewTask(func() { log.Println("One-time job executed!") }), + []gocron.JobOption{gocron.WithName("one-time-job"), gocron.WithTags("onetime", "scheduled")}, + }, + { + gocron.DurationJob(20 * time.Second), + gocron.NewTask(func() { + items := rand.Intn(100) + 1 + log.Printf("Processing %d items...", items) + time.Sleep(2 * time.Second) + log.Printf("Successfully processed %d items", items) + }), + []gocron.JobOption{gocron.WithName("data-processor-job"), gocron.WithTags("processing", "batch")}, + }, + { + gocron.DurationJob(30 * time.Second), + gocron.NewTask(func() { + status := "healthy" + if rand.Float32() < 0.1 { + status = "degraded" + } + log.Printf("Health check: System is %s", status) + }), + []gocron.JobOption{gocron.WithName("health-check-job"), gocron.WithTags("monitoring", "health")}, + }, +} + +func main() { + port := flag.Int("port", 8080, "Port to run the server on") + title := flag.String("title", "GoCron Scheduler", "Custom title for the UI") + flag.Parse() + + // create the gocron scheduler + scheduler, err := gocron.NewScheduler() + if err != nil { + log.Fatalf("Failed to create scheduler: %v", err) + } + + // add jobs to the scheduler + for _, job := range jobs { + if _, err := scheduler.NewJob(job.definition, job.task, job.options...); err != nil { + log.Printf("Error creating job: %v", err) + } + } + + // start the scheduler + scheduler.Start() + log.Println("Scheduler started with", len(scheduler.Jobs()), "jobs") + + // create and start the API server with custom title + srv := server.NewServer(scheduler, *port, server.WithTitle(*title)) + + // start server in a goroutine + go func() { + addr := fmt.Sprintf(":%d", *port) + log.Println("\n" + strings.Repeat("=", 70)) + log.Printf("GoCron UI Server Started") + log.Println(strings.Repeat("=", 70)) + log.Printf("Web UI: http://localhost%s", addr) + log.Printf("API: http://localhost%s/api", addr) + log.Printf("WebSocket: ws://localhost%s/ws", addr) + log.Printf("Total Jobs: %d", len(scheduler.Jobs())) + log.Println(strings.Repeat("=", 70) + "\n") + + if err := http.ListenAndServe(addr, srv.Router); err != nil { + log.Fatalf("Server failed to start: %v", err) + } + }() + + // wait for interrupt signal to gracefully shutdown + quit := make(chan os.Signal, 1) + signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) + <-quit + + log.Println("\nShutting down server...") + + // shutdown scheduler + if err := scheduler.Shutdown(); err != nil { + log.Printf("Error shutting down scheduler: %v", err) + } + + log.Println("Server stopped gracefully") +} diff --git a/examples/main.go b/examples/main.go deleted file mode 100644 index 0df6cb6..0000000 --- a/examples/main.go +++ /dev/null @@ -1,276 +0,0 @@ -package main - -import ( - "context" - "flag" - "fmt" - "log" - "math/rand" - "net/http" - "os" - "os/signal" - "strings" - "syscall" - "time" - - "github.com/go-co-op/gocron-ui/server" - "github.com/go-co-op/gocron/v2" - "github.com/google/uuid" -) - -func main() { - port := flag.Int("port", 8080, "Port to run the server on") - title := flag.String("title", "GoCron Scheduler", "Custom title for the UI") - flag.Parse() - - // create the gocron scheduler - scheduler, err := gocron.NewScheduler() - if err != nil { - log.Fatalf("Failed to create scheduler: %v", err) - } - - // example 1: Simple interval job - runs every 10 seconds - _, err = scheduler.NewJob( - gocron.DurationJob(10*time.Second), - gocron.NewTask(func() { - log.Println("Running 10-second interval job") - }), - gocron.WithName("simple-10s-interval"), - gocron.WithTags("interval", "simple"), - ) - if err != nil { - log.Printf("Error creating simple interval job: %v", err) - } - - // example 2: Fast job - runs every 5 seconds - _, err = scheduler.NewJob( - gocron.DurationJob(5*time.Second), - gocron.NewTask(func() { - log.Println("Fast 5-second job executed") - }), - gocron.WithName("fast-5s-job"), - gocron.WithTags("interval", "fast"), - ) - if err != nil { - log.Printf("Error creating fast job: %v", err) - } - - // example 3: Cron job - runs every minute - _, err = scheduler.NewJob( - gocron.CronJob("* * * * *", false), - gocron.NewTask(func() { - log.Println("Cron job executed (every minute)") - }), - gocron.WithName("cron-every-minute"), - gocron.WithTags("cron", "periodic"), - ) - if err != nil { - log.Printf("Error creating cron job: %v", err) - } - - // example 4: Daily job at specific time - _, err = scheduler.NewJob( - gocron.DailyJob(1, gocron.NewAtTimes( - gocron.NewAtTime(14, 30, 0), // 2:30 PM - )), - gocron.NewTask(func() { - log.Println("Daily job executed at 2:30 PM") - }), - gocron.WithName("daily-afternoon-report"), - gocron.WithTags("daily", "report"), - ) - if err != nil { - log.Printf("Error creating daily job: %v", err) - } - - // example 5: Weekly job - runs on specific days - _, err = scheduler.NewJob( - gocron.WeeklyJob(1, gocron.NewWeekdays(time.Monday, time.Wednesday, time.Friday), - gocron.NewAtTimes(gocron.NewAtTime(9, 0, 0))), - gocron.NewTask(func() { - log.Println("Weekly job executed (Mon, Wed, Fri at 9:00 AM)") - }), - gocron.WithName("weekly-mwf-morning"), - gocron.WithTags("weekly", "morning", "report"), - ) - if err != nil { - log.Printf("Error creating weekly job: %v", err) - } - - // example 6: Job with parameters - _, err = scheduler.NewJob( - gocron.DurationJob(12*time.Second), - gocron.NewTask(func(name string, count int) { - log.Printf("Job with parameters: name=%s, count=%d", name, count) - }, "example-job", 42), - gocron.WithName("parameterized-job"), - gocron.WithTags("parameters", "demo"), - ) - if err != nil { - log.Printf("Error creating parameterized job: %v", err) - } - - // example 7: Job with context - _, err = scheduler.NewJob( - gocron.DurationJob(8*time.Second), - gocron.NewTask(func(ctx context.Context) { - log.Printf("Job with context executed, context: %v", ctx) - }), - gocron.WithName("context-aware-job"), - gocron.WithTags("context", "advanced"), - ) - if err != nil { - log.Printf("Error creating context job: %v", err) - } - - // example 8: Random duration job - _, err = scheduler.NewJob( - gocron.DurationRandomJob(5*time.Second, 15*time.Second), - gocron.NewTask(func() { - log.Println("Random interval job executed (5-15 seconds)") - }), - gocron.WithName("random-interval-job"), - gocron.WithTags("random", "variable"), - ) - if err != nil { - log.Printf("Error creating random job: %v", err) - } - - // example 9: Job with singleton mode (prevents overlapping runs) - _, err = scheduler.NewJob( - gocron.DurationJob(5*time.Second), - gocron.NewTask(func() { - log.Println("Singleton job started") - time.Sleep(8 * time.Second) // Simulate long-running task - log.Println("Singleton job completed") - }), - gocron.WithName("singleton-mode-job"), - gocron.WithTags("singleton", "long-running"), - gocron.WithSingletonMode(gocron.LimitModeReschedule), - ) - if err != nil { - log.Printf("Error creating singleton job: %v", err) - } - - // example 10: Limited run job (runs only 3 times) - _, err = scheduler.NewJob( - gocron.DurationJob(7*time.Second), - gocron.NewTask(func() { - log.Println("Limited run job executed") - }), - gocron.WithName("limited-run-job"), - gocron.WithTags("limited", "demo"), - gocron.WithLimitedRuns(3), - ) - if err != nil { - log.Printf("Error creating limited run job: %v", err) - } - - // example 11: Job with event listeners - _, err = scheduler.NewJob( - gocron.DurationJob(15*time.Second), - gocron.NewTask(func() { - log.Println("Job with listeners executed") - // Simulate some work - time.Sleep(time.Duration(rand.Intn(3)+1) * time.Second) - }), - gocron.WithName("event-listener-job"), - gocron.WithTags("events", "monitoring"), - gocron.WithEventListeners( - gocron.AfterJobRuns(func(_ uuid.UUID, jobName string) { - log.Printf(" → AfterJobRuns: %s completed", jobName) - }), - gocron.BeforeJobRuns(func(_ uuid.UUID, jobName string) { - log.Printf(" → BeforeJobRuns: %s starting", jobName) - }), - ), - ) - if err != nil { - log.Printf("Error creating event listener job: %v", err) - } - - // example 12: One-time job (runs once at a specific time) - oneTimeAt := time.Now().Add(30 * time.Second) - _, err = scheduler.NewJob( - gocron.OneTimeJob(gocron.OneTimeJobStartDateTime(oneTimeAt)), - gocron.NewTask(func() { - log.Println("One-time job executed!") - }), - gocron.WithName("one-time-job"), - gocron.WithTags("onetime", "scheduled"), - ) - if err != nil { - log.Printf("Error creating one-time job: %v", err) - } - - // example 13: Job that simulates data processing - _, err = scheduler.NewJob( - gocron.DurationJob(20*time.Second), - gocron.NewTask(func() { - items := rand.Intn(100) + 1 - log.Printf("Processing %d items...", items) - time.Sleep(2 * time.Second) - log.Printf("Successfully processed %d items", items) - }), - gocron.WithName("data-processor-job"), - gocron.WithTags("processing", "batch"), - ) - if err != nil { - log.Printf("Error creating data processor job: %v", err) - } - - // example 14: Health check job - _, err = scheduler.NewJob( - gocron.DurationJob(30*time.Second), - gocron.NewTask(func() { - status := "healthy" - if rand.Float32() < 0.1 { // 10% chance of unhealthy - status = "degraded" - } - log.Printf("Health check: System is %s", status) - }), - gocron.WithName("health-check-job"), - gocron.WithTags("monitoring", "health"), - ) - if err != nil { - log.Printf("Error creating health check job: %v", err) - } - - // start the scheduler - scheduler.Start() - log.Println("Scheduler started with", len(scheduler.Jobs()), "jobs") - - // create and start the API server with custom title - srv := server.NewServer(scheduler, *port, server.WithTitle(*title)) - - // start server in a goroutine - go func() { - addr := fmt.Sprintf(":%d", *port) - log.Println("\n" + strings.Repeat("=", 70)) - log.Printf("GoCron UI Server Started") - log.Println(strings.Repeat("=", 70)) - log.Printf("Web UI: http://localhost%s", addr) - log.Printf("API: http://localhost%s/api", addr) - log.Printf("WebSocket: ws://localhost%s/ws", addr) - log.Printf("Total Jobs: %d", len(scheduler.Jobs())) - log.Println(strings.Repeat("=", 70) + "\n") - - if err := http.ListenAndServe(addr, srv.Router); err != nil { - log.Fatalf("Server failed to start: %v", err) - } - }() - - // wait for interrupt signal to gracefully shutdown - quit := make(chan os.Signal, 1) - signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) - <-quit - - log.Println("\nShutting down server...") - - // shutdown scheduler - if err := scheduler.Shutdown(); err != nil { - log.Printf("Error shutting down scheduler: %v", err) - } - - log.Println("Server stopped gracefully") -}