diff --git a/Makefile b/Makefile
index 5f83aad..ff139ae 100644
--- a/Makefile
+++ b/Makefile
@@ -1,9 +1,10 @@
.PHONY: all build run test clean
BINARY_NAME=hireme
-DB_PATH=data/db/hireme.db
CONFIG_PATH=data/config/targets.yaml
PORT=8080
+ENV=dev
+DB_PATH=data/db/$(ENV)/hireme.db
all: build
@@ -20,4 +21,4 @@ test:
clean:
rm -f build/$(BINARY_NAME)
- rm -rf data/db/*.db
+ rm -rf data/db/$(ENV)/*.db
diff --git a/cmd/hireme/main.go b/cmd/hireme/main.go
index 686b99f..733533d 100644
--- a/cmd/hireme/main.go
+++ b/cmd/hireme/main.go
@@ -2,7 +2,6 @@ package main
import (
"fmt"
- "log"
"net/http"
"os"
"path/filepath"
@@ -10,6 +9,7 @@ import (
"github.com/hireme/hireme/internal/analyzer"
"github.com/hireme/hireme/internal/config"
"github.com/hireme/hireme/internal/db"
+ "github.com/hireme/hireme/internal/logger"
"github.com/hireme/hireme/internal/ui"
"github.com/hireme/hireme/internal/worker"
"github.com/spf13/cobra"
@@ -25,11 +25,13 @@ func newRootCmd() *cobra.Command {
RunE: func(cmd *cobra.Command, args []string) error {
cfg.Normalize()
- log.Println("==========================================================")
- log.Println("🚀 Starting Hireme (GJCC) Job Application Command Center")
- log.Println("==========================================================")
- log.Printf("[Config] Port: %s | DB Path: %s | Target Config: %s | Active LLM: %s",
- cfg.Port, cfg.DBPath, cfg.TargetConfig, cfg.LLMProvider)
+ logger.SetLevel(cfg.LogLevel)
+
+ logger.Log.Info("==========================================================")
+ logger.Log.Info("🚀 Starting Hireme (GJCC) Job Application Command Center")
+ logger.Log.Info("==========================================================")
+ logger.Log.Infof("[Config] Port: %s | DB Path: %s | Target Config: %s | Active LLM: %s | Log Level: %s",
+ cfg.Port, cfg.DBPath, cfg.TargetConfig, cfg.LLMProvider, cfg.LogLevel)
// Ensure DB directory exists
dbDir := filepath.Dir(cfg.DBPath)
@@ -46,7 +48,7 @@ func newRootCmd() *cobra.Command {
// Load targets from YAML config if database is empty
if err := database.LoadTargetsFromYAML(cfg.TargetConfig); err != nil {
- log.Printf("[Main] Target YAML loading status: %v", err)
+ logger.Log.Errorf("[Main] Target YAML loading status: %v", err)
}
// Initialize LLM Analyzer
@@ -55,8 +57,14 @@ func newRootCmd() *cobra.Command {
return fmt.Errorf("fatal Analyzer setup error: %w", err)
}
- // Initialize Worker Pool
- pool := worker.NewWorkerPool(database, llmAnalyzer, 3)
+ // Load filter configuration
+ filterCfg, err := config.LoadFilterConfig(cfg.FilterConfig)
+ if err != nil {
+ logger.Log.Fatalf("Failed to load filter config: %v", err)
+ }
+
+ // 4. Initialize Worker Pool
+ pool := worker.NewWorkerPool(database, llmAnalyzer, 3, filterCfg)
pool.Start() // Auto start the worker pool
// Setup UI Server & Routes
@@ -65,7 +73,7 @@ func newRootCmd() *cobra.Command {
server.RegisterRoutes(mux)
addr := fmt.Sprintf(":%s", cfg.Port)
- log.Printf("✨ Hireme Command Center is live at http://localhost:%s", cfg.Port)
+ logger.Log.Infof("✨ Hireme Command Center is live at http://localhost:%s", cfg.Port)
return http.ListenAndServe(addr, mux)
},
}
diff --git a/data/config/filters.yaml b/data/config/filters.yaml
new file mode 100644
index 0000000..0a296e1
--- /dev/null
+++ b/data/config/filters.yaml
@@ -0,0 +1,31 @@
+hard_keywords:
+ - "no sponsorship"
+ - "us citizen only"
+ - "unpaid"
+ - "volunteer"
+
+scoring_weights:
+ - keyword: golang
+ weight: 3.0
+ - keyword: go
+ weight: 2.0
+ - keyword: python
+ weight: 2.0
+ - keyword: kubernetes
+ weight: 3.0
+ - keyword: k8s
+ weight: 3.0
+ - keyword: docker
+ weight: 2.0
+ - keyword: aws
+ weight: 1.0
+ - keyword: gcp
+ weight: 1.0
+ - keyword: distributed systems
+ weight: 4.0
+ - keyword: backend
+ weight: 3.0
+ - keyword: ai
+ weight: -5.0
+ - keyword: machine learning
+ weight: -5.0
diff --git a/go.mod b/go.mod
index 0e2fa19..ecc14e4 100644
--- a/go.mod
+++ b/go.mod
@@ -10,4 +10,8 @@ require (
gopkg.in/yaml.v3 v3.0.1
)
-require github.com/inconshreveable/mousetrap v1.1.0 // indirect
+require (
+ github.com/inconshreveable/mousetrap v1.1.0 // indirect
+ go.uber.org/multierr v1.10.0 // indirect
+ go.uber.org/zap v1.28.0 // indirect
+)
diff --git a/go.sum b/go.sum
index 0492c1b..dc4bf09 100644
--- a/go.sum
+++ b/go.sum
@@ -8,6 +8,10 @@ github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
+go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
+go.uber.org/zap v1.28.0 h1:IZzaP1Fv73/T/pBMLk4VutPl36uNC+OSUh3JLG3FIjo=
+go.uber.org/zap v1.28.0/go.mod h1:rDLpOi171uODNm/mxFcuYWxDsqWSAVkFdX4XojSKg/Q=
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
diff --git a/internal/config/config.go b/internal/config/config.go
index 96409d0..99842ed 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -11,6 +11,8 @@ type Config struct {
Port string
DBPath string
TargetConfig string
+ FilterConfig string
+ LogLevel string
LLMProvider string // "gemini", "deepseek", "mock"
GeminiAPIKey string
DeepSeekAPIKey string
@@ -23,6 +25,8 @@ func RegisterFlags(flags *pflag.FlagSet) *Config {
flags.StringVarP(&cfg.Port, "port", "p", getEnvOrDefault("PORT", "8080"), "HTTP server port")
flags.StringVarP(&cfg.DBPath, "db", "d", getEnvOrDefault("DB_PATH", "data/db/hireme.db"), "Path to database file")
flags.StringVarP(&cfg.TargetConfig, "target-config", "c", getEnvOrDefault("TARGET_CONFIG", "data/config/targets.yaml"), "Path to targets YAML file")
+ flags.StringVarP(&cfg.FilterConfig, "filter-config", "f", getEnvOrDefault("FILTER_CONFIG", "data/config/filters.yaml"), "Path to filters YAML file")
+ flags.StringVar(&cfg.LogLevel, "log-level", getEnvOrDefault("LOG_LEVEL", "info"), "Logging level (trace, debug, info, error)")
flags.StringVarP(&cfg.LLMProvider, "llm-provider", "l", getEnvOrDefault("LLM_PROVIDER", ""), "Active LLM Provider (gemini, deepseek, mock)")
flags.StringVar(&cfg.GeminiAPIKey, "gemini-api-key", os.Getenv("GEMINI_API_KEY"), "Gemini API Key")
flags.StringVar(&cfg.DeepSeekAPIKey, "deepseek-api-key", os.Getenv("DEEPSEEK_API_KEY"), "DeepSeek API Key")
diff --git a/internal/config/filters.go b/internal/config/filters.go
new file mode 100644
index 0000000..968442d
--- /dev/null
+++ b/internal/config/filters.go
@@ -0,0 +1,28 @@
+package config
+
+import (
+ "fmt"
+ "os"
+
+ "github.com/hireme/hireme/internal/filter"
+ "gopkg.in/yaml.v3"
+)
+
+type FilterConfig struct {
+ HardKeywords []string `yaml:"hard_keywords"`
+ ScoringWeights []filter.KeywordWeight `yaml:"scoring_weights"`
+}
+
+func LoadFilterConfig(path string) (*FilterConfig, error) {
+ data, err := os.ReadFile(path)
+ if err != nil {
+ return nil, fmt.Errorf("failed to read filter config file %s: %w", path, err)
+ }
+
+ var cfg FilterConfig
+ if err := yaml.Unmarshal(data, &cfg); err != nil {
+ return nil, fmt.Errorf("failed to unmarshal filter config: %w", err)
+ }
+
+ return &cfg, nil
+}
diff --git a/internal/db/db.go b/internal/db/db.go
index 869fef4..289c4e8 100644
--- a/internal/db/db.go
+++ b/internal/db/db.go
@@ -17,7 +17,7 @@ func InitDB(dbPath string) (*DB, error) {
dbPath = "data/db/hireme.db"
}
- conn, err := sql.Open("sqlite3", dbPath+"?_foreign_keys=on")
+ conn, err := sql.Open("sqlite3", dbPath+"?_foreign_keys=on&_busy_timeout=5000&_journal_mode=WAL&_txlock=immediate")
if err != nil {
return nil, fmt.Errorf("failed to open sqlite database: %w", err)
}
diff --git a/internal/db/tasks.go b/internal/db/tasks.go
index 976d375..1c19392 100644
--- a/internal/db/tasks.go
+++ b/internal/db/tasks.go
@@ -24,24 +24,21 @@ func (db *DB) EnqueueTask(taskType string, payload interface{}) error {
// ClaimNextTask atomically finds and claims the next pending task
func (db *DB) ClaimNextTask() (*Task, error) {
- tx, err := db.Begin()
- if err != nil {
- return nil, err
- }
- defer func() { _ = tx.Rollback() }()
-
- // Find the oldest PENDING task that is ready to be retried
query := `
- SELECT id, task_type, payload, status, error_message, retry_count, next_retry_at, created_at, updated_at
- FROM tasks
- WHERE status = 'PENDING' AND next_retry_at <= CURRENT_TIMESTAMP
- ORDER BY created_at ASC
- LIMIT 1
+ UPDATE tasks
+ SET status = 'RUNNING', updated_at = CURRENT_TIMESTAMP
+ WHERE id = (
+ SELECT id FROM tasks
+ WHERE status = 'PENDING' AND next_retry_at <= CURRENT_TIMESTAMP
+ ORDER BY created_at ASC
+ LIMIT 1
+ )
+ RETURNING id, task_type, payload, status, error_message, retry_count, next_retry_at, created_at, updated_at
`
var task Task
var errMsg sql.NullString
- err = tx.QueryRow(query).Scan(
+ err := db.QueryRow(query).Scan(
&task.ID, &task.TaskType, &task.Payload, &task.Status, &errMsg,
&task.RetryCount, &task.NextRetryAt, &task.CreatedAt, &task.UpdatedAt,
)
@@ -57,17 +54,6 @@ func (db *DB) ClaimNextTask() (*Task, error) {
task.ErrorMessage = errMsg.String
}
- // Claim the task
- updateQuery := `UPDATE tasks SET status = 'RUNNING', updated_at = CURRENT_TIMESTAMP WHERE id = ?`
- _, err = tx.Exec(updateQuery, task.ID)
- if err != nil {
- return nil, err
- }
-
- if err := tx.Commit(); err != nil {
- return nil, err
- }
-
return &task, nil
}
@@ -157,3 +143,49 @@ func (db *DB) GetQueueStatus() (map[string]int, error) {
}
return counts, nil
}
+
+// ReprocessAllJobs wipes all job analysis results and re-queues PROCESS tasks for all existing raw jobs.
+func (db *DB) ReprocessAllJobs() error {
+ tx, err := db.Begin()
+ if err != nil {
+ return err
+ }
+ defer func() { _ = tx.Rollback() }()
+
+ // 1. Delete all rows from job_analyses
+ if _, err := tx.Exec(`DELETE FROM job_analyses`); err != nil {
+ return fmt.Errorf("failed to clear job_analyses: %w", err)
+ }
+
+ // 2. Delete all existing PROCESS and ANALYZE tasks
+ if _, err := tx.Exec(`DELETE FROM tasks WHERE task_type IN ('PROCESS', 'ANALYZE')`); err != nil {
+ return fmt.Errorf("failed to clear old tasks: %w", err)
+ }
+
+ // 3. Queue up new PROCESS tasks for all raw jobs
+ rows, err := tx.Query(`SELECT id FROM raw_jobs`)
+ if err != nil {
+ return fmt.Errorf("failed to get raw jobs: %w", err)
+ }
+ defer func() { _ = rows.Close() }()
+
+ var rawJobIDs []int64
+ for rows.Next() {
+ var rawJobID int64
+ if err := rows.Scan(&rawJobID); err != nil {
+ return err
+ }
+ rawJobIDs = append(rawJobIDs, rawJobID)
+ }
+ rows.Close() // Explicitly close to release any read locks
+
+ // 4. Enqueue PROCESS task for each job
+ for _, rawJobID := range rawJobIDs {
+ payloadJSON := fmt.Sprintf(`{"raw_job_id":%d}`, rawJobID)
+ if _, err := tx.Exec(`INSERT INTO tasks (task_type, payload, status) VALUES ('PROCESS', ?, 'PENDING')`, payloadJSON); err != nil {
+ return fmt.Errorf("failed to enqueue process task for job %d: %w", rawJobID, err)
+ }
+ }
+
+ return tx.Commit()
+}
diff --git a/internal/filter/filter.go b/internal/filter/filter.go
index 7567750..e8d16d7 100644
--- a/internal/filter/filter.go
+++ b/internal/filter/filter.go
@@ -1,6 +1,10 @@
package filter
-import "context"
+import (
+ "context"
+
+ "github.com/hireme/hireme/internal/db"
+)
type FilterResult struct {
Keep bool
@@ -10,7 +14,7 @@ type FilterResult struct {
type JobFilter interface {
Name() string
- Process(ctx context.Context, title, rawContent string) (FilterResult, error)
+ Process(ctx context.Context, job *db.RawJob) (FilterResult, error)
}
type FilterPipeline struct {
@@ -21,10 +25,10 @@ func NewPipeline(filters ...JobFilter) *FilterPipeline {
return &FilterPipeline{filters: filters}
}
-func (p *FilterPipeline) Execute(ctx context.Context, title, rawContent string) (bool, float64, string) {
+func (p *FilterPipeline) Execute(ctx context.Context, job *db.RawJob) (bool, float64, string) {
var totalScore float64
for _, f := range p.filters {
- res, err := f.Process(ctx, title, rawContent)
+ res, err := f.Process(ctx, job)
if err != nil {
return false, 0, err.Error()
}
diff --git a/internal/filter/filter_test.go b/internal/filter/filter_test.go
index e81de0c..3593090 100644
--- a/internal/filter/filter_test.go
+++ b/internal/filter/filter_test.go
@@ -3,6 +3,8 @@ package filter
import (
"context"
"testing"
+
+ "github.com/hireme/hireme/internal/db"
)
func TestHardKeywordFilter(t *testing.T) {
@@ -10,7 +12,7 @@ func TestHardKeywordFilter(t *testing.T) {
fk := NewHardKeywordFilter([]string{"DevOps", "No Sponsorship"})
// Should fail due to DevOps
- res1, err := fk.Process(ctx, "Senior DevOps Engineer", "We build pipelines.")
+ res1, err := fk.Process(ctx, &db.RawJob{Title: "Senior DevOps Engineer", RawContent: "We build pipelines."})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
@@ -19,7 +21,7 @@ func TestHardKeywordFilter(t *testing.T) {
}
// Should pass
- res2, err := fk.Process(ctx, "Senior Backend Engineer", "We build scalable APIs in Go.")
+ res2, err := fk.Process(ctx, &db.RawJob{Title: "Senior Backend Engineer", RawContent: "We build scalable APIs in Go."})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
@@ -39,7 +41,7 @@ func TestFilterPipeline(t *testing.T) {
pipeline := NewPipeline(hardFilter, scoringFilter)
// Test job 1: Unpaid job -> rejected by hard filter
- keep1, score1, _ := pipeline.Execute(ctx, "Backend Intern", "Unpaid position working with Go.")
+ keep1, score1, _ := pipeline.Execute(ctx, &db.RawJob{Title: "Backend Intern", RawContent: "Unpaid position working with Go."})
if keep1 {
t.Errorf("expected job 1 to be filtered out, got keep=true")
}
@@ -48,7 +50,7 @@ func TestFilterPipeline(t *testing.T) {
}
// Test job 2: Qualified job -> passes both filters and accumulates score
- keep2, score2, _ := pipeline.Execute(ctx, "Senior Go Backend Engineer", "Building high throughput services.")
+ keep2, score2, _ := pipeline.Execute(ctx, &db.RawJob{Title: "Senior Go Backend Engineer", RawContent: "Building high throughput services."})
if !keep2 {
t.Errorf("expected job 2 to pass filter pipeline, got keep=false")
}
diff --git a/internal/filter/hard_keyword.go b/internal/filter/hard_keyword.go
index ff8a22e..4fce203 100644
--- a/internal/filter/hard_keyword.go
+++ b/internal/filter/hard_keyword.go
@@ -4,6 +4,8 @@ import (
"context"
"fmt"
"strings"
+
+ "github.com/hireme/hireme/internal/db"
)
type HardKeywordFilter struct {
@@ -26,8 +28,8 @@ func (f *HardKeywordFilter) Name() string {
return "HardKeywordFilter"
}
-func (f *HardKeywordFilter) Process(ctx context.Context, title, rawContent string) (FilterResult, error) {
- combined := strings.ToLower(title + " " + rawContent)
+func (f *HardKeywordFilter) Process(ctx context.Context, job *db.RawJob) (FilterResult, error) {
+ combined := strings.ToLower(job.Title + " " + job.RawContent)
for _, kw := range f.Blacklist {
term := strings.ToLower(strings.TrimSpace(kw))
diff --git a/internal/filter/scoring.go b/internal/filter/scoring.go
index b6fa199..7302245 100644
--- a/internal/filter/scoring.go
+++ b/internal/filter/scoring.go
@@ -4,6 +4,8 @@ import (
"context"
"fmt"
"strings"
+
+ "github.com/hireme/hireme/internal/db"
)
type KeywordWeight struct {
@@ -38,14 +40,14 @@ func (s *ScoringFilter) Name() string {
return "ScoringFilter"
}
-func (s *ScoringFilter) Process(ctx context.Context, title, rawContent string) (FilterResult, error) {
- combined := strings.ToLower(title + " " + rawContent)
+func (f *ScoringFilter) Process(ctx context.Context, job *db.RawJob) (FilterResult, error) {
+ combinedLower := strings.ToLower(job.Title + " " + job.RawContent)
var totalScore float64
var matched []string
- for _, kw := range s.Weights {
+ for _, kw := range f.Weights {
term := strings.ToLower(strings.TrimSpace(kw.Keyword))
- if term != "" && strings.Contains(combined, term) {
+ if term != "" && strings.Contains(combinedLower, term) {
totalScore += kw.Weight
matched = append(matched, kw.Keyword)
}
diff --git a/internal/logger/logger.go b/internal/logger/logger.go
new file mode 100644
index 0000000..c0a955b
--- /dev/null
+++ b/internal/logger/logger.go
@@ -0,0 +1,44 @@
+package logger
+
+import (
+ "os"
+ "strings"
+
+ "go.uber.org/zap"
+ "go.uber.org/zap/zapcore"
+)
+
+var Log *zap.SugaredLogger
+
+func init() {
+ levelStr := os.Getenv("LOG_LEVEL")
+ if levelStr == "" {
+ levelStr = "info"
+ }
+ SetLevel(levelStr)
+}
+
+func SetLevel(levelStr string) {
+ var level zapcore.Level
+ switch strings.ToLower(levelStr) {
+ case "trace", "debug":
+ level = zap.DebugLevel
+ case "info":
+ level = zap.InfoLevel
+ case "warn":
+ level = zap.WarnLevel
+ case "error":
+ level = zap.ErrorLevel
+ default:
+ level = zap.InfoLevel
+ }
+
+ config := zap.NewDevelopmentConfig()
+ config.Level = zap.NewAtomicLevelAt(level)
+ config.EncoderConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder
+ // Customize the timestamp format if needed
+ config.EncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
+
+ logger, _ := config.Build()
+ Log = logger.Sugar()
+}
diff --git a/internal/ui/handlers.go b/internal/ui/handlers.go
index a0cbbbb..0255c2a 100644
--- a/internal/ui/handlers.go
+++ b/internal/ui/handlers.go
@@ -1,13 +1,13 @@
package ui
import (
- "log"
"net/http"
"strconv"
"strings"
"github.com/hireme/hireme/internal/config"
"github.com/hireme/hireme/internal/db"
+ "github.com/hireme/hireme/internal/logger"
"github.com/hireme/hireme/internal/worker"
)
@@ -37,6 +37,7 @@ func (s *Server) RegisterRoutes(mux *http.ServeMux) {
mux.HandleFunc("PATCH /applications/{id}/status", s.handleUpdateAppStatus)
mux.HandleFunc("POST /api/tasks/discover", s.handleDiscoverTasks)
mux.HandleFunc("POST /api/tasks/clean", s.handleCleanTasks)
+ mux.HandleFunc("POST /api/tasks/reprocess", s.handleReprocessJobs)
mux.HandleFunc("POST /api/tasks/toggle", s.handleToggleTasks)
}
@@ -84,7 +85,7 @@ func (s *Server) handleIndex(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
if err := RenderPage(w, data); err != nil {
- log.Printf("[UI] RenderPage error: %v", err)
+ logger.Log.Errorf("[UI] RenderPage error: %v", err)
}
}
@@ -273,6 +274,7 @@ func (s *Server) handleUpdateAppStatus(w http.ResponseWriter, r *http.Request) {
}
func (s *Server) handleDiscoverTasks(w http.ResponseWriter, r *http.Request) {
+ logger.Log.Info("[API] Triggered /api/tasks/discover")
targets, err := s.db.GetAllTargets()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
@@ -299,6 +301,7 @@ func (s *Server) handleDiscoverTasks(w http.ResponseWriter, r *http.Request) {
}
func (s *Server) handleCleanTasks(w http.ResponseWriter, r *http.Request) {
+ logger.Log.Info("[API] Triggered /api/tasks/clean")
err := s.db.ClearTaskQueue()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
@@ -308,7 +311,19 @@ func (s *Server) handleCleanTasks(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte(`{"status": "success"}`))
}
+func (s *Server) handleReprocessJobs(w http.ResponseWriter, r *http.Request) {
+ logger.Log.Info("[API] Triggered /api/tasks/reprocess")
+ if err := s.db.ReprocessAllJobs(); err != nil {
+ logger.Log.Errorf("[API] Failed to reprocess jobs: %v", err)
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ w.Header().Set("Content-Type", "application/json")
+ _, _ = w.Write([]byte(`{"status": "success", "message": "All jobs queued for reprocessing"}`))
+}
+
func (s *Server) handleToggleTasks(w http.ResponseWriter, r *http.Request) {
+ logger.Log.Info("[API] Triggered /api/tasks/toggle")
isRunning := s.pool.Toggle()
status := "paused"
if isRunning {
diff --git a/internal/ui/templates.go b/internal/ui/templates.go
index 8a2a42f..59cecf9 100644
--- a/internal/ui/templates.go
+++ b/internal/ui/templates.go
@@ -193,6 +193,12 @@ const mainLayoutHTML = `{{ define "page" }}
class="relative inline-flex items-center justify-center px-3 py-1.5 text-xs font-semibold text-white transition-all bg-red-600 rounded-lg shadow-md hover:bg-red-500 active:scale-95 shadow-red-500/25">
🗑️ Clean Queue
+