package storage import ( "database/sql" "fmt" "time" ) // ScheduledJob represents a cron-based recurring scan job. type ScheduledJob struct { ID int64 Name string CronExpr string ScanPath string Enabled bool Notify bool LastRunAt *time.Time NextRunAt *time.Time CreatedAt time.Time } // SaveScheduledJob inserts a new scheduled job and returns its ID. func (db *DB) SaveScheduledJob(job ScheduledJob) (int64, error) { enabledInt := 0 if job.Enabled { enabledInt = 1 } notifyInt := 0 if job.Notify { notifyInt = 1 } res, err := db.sql.Exec( `INSERT INTO scheduled_jobs (name, cron_expr, scan_path, enabled, notify) VALUES (?, ?, ?, ?, ?)`, job.Name, job.CronExpr, job.ScanPath, enabledInt, notifyInt, ) if err != nil { return 0, fmt.Errorf("inserting scheduled job: %w", err) } return res.LastInsertId() } // ListScheduledJobs returns all scheduled jobs ordered by creation time. func (db *DB) ListScheduledJobs() ([]ScheduledJob, error) { rows, err := db.sql.Query( `SELECT id, name, cron_expr, scan_path, enabled, notify, last_run_at, next_run_at, created_at FROM scheduled_jobs ORDER BY created_at ASC`, ) if err != nil { return nil, fmt.Errorf("querying scheduled jobs: %w", err) } defer rows.Close() var jobs []ScheduledJob for rows.Next() { j, err := scanJobRow(rows) if err != nil { return nil, err } jobs = append(jobs, j) } return jobs, rows.Err() } // GetScheduledJob returns a single job by ID. func (db *DB) GetScheduledJob(id int64) (*ScheduledJob, error) { row := db.sql.QueryRow( `SELECT id, name, cron_expr, scan_path, enabled, notify, last_run_at, next_run_at, created_at FROM scheduled_jobs WHERE id = ?`, id, ) var j ScheduledJob var enabledInt, notifyInt int var lastRun, nextRun, createdAt sql.NullString if err := row.Scan(&j.ID, &j.Name, &j.CronExpr, &j.ScanPath, &enabledInt, ¬ifyInt, &lastRun, &nextRun, &createdAt); err != nil { return nil, err } j.Enabled = enabledInt != 0 j.Notify = notifyInt != 0 j.LastRunAt = parseNullTime(lastRun) j.NextRunAt = parseNullTime(nextRun) if createdAt.Valid { t, _ := time.Parse("2006-01-02 15:04:05", createdAt.String) j.CreatedAt = t } return &j, nil } // DeleteScheduledJob removes a job by ID and returns rows affected. func (db *DB) DeleteScheduledJob(id int64) (int64, error) { res, err := db.sql.Exec(`DELETE FROM scheduled_jobs WHERE id = ?`, id) if err != nil { return 0, fmt.Errorf("deleting scheduled job %d: %w", id, err) } return res.RowsAffected() } // UpdateJobLastRun updates the last_run_at timestamp for a job. func (db *DB) UpdateJobLastRun(id int64, t time.Time) error { _, err := db.sql.Exec( `UPDATE scheduled_jobs SET last_run_at = ? WHERE id = ?`, t.Format("2006-01-02 15:04:05"), id, ) return err } // ListEnabledScheduledJobs returns only enabled jobs. func (db *DB) ListEnabledScheduledJobs() ([]ScheduledJob, error) { rows, err := db.sql.Query( `SELECT id, name, cron_expr, scan_path, enabled, notify, last_run_at, next_run_at, created_at FROM scheduled_jobs WHERE enabled = 1 ORDER BY created_at ASC`, ) if err != nil { return nil, fmt.Errorf("querying enabled scheduled jobs: %w", err) } defer rows.Close() var jobs []ScheduledJob for rows.Next() { j, err := scanJobRow(rows) if err != nil { return nil, err } jobs = append(jobs, j) } return jobs, rows.Err() } func scanJobRow(rows *sql.Rows) (ScheduledJob, error) { var j ScheduledJob var enabledInt, notifyInt int var lastRun, nextRun, createdAt sql.NullString if err := rows.Scan(&j.ID, &j.Name, &j.CronExpr, &j.ScanPath, &enabledInt, ¬ifyInt, &lastRun, &nextRun, &createdAt); err != nil { return j, fmt.Errorf("scanning scheduled job row: %w", err) } j.Enabled = enabledInt != 0 j.Notify = notifyInt != 0 j.LastRunAt = parseNullTime(lastRun) j.NextRunAt = parseNullTime(nextRun) if createdAt.Valid { t, _ := time.Parse("2006-01-02 15:04:05", createdAt.String) j.CreatedAt = t } return j, nil } func parseNullTime(ns sql.NullString) *time.Time { if !ns.Valid || ns.String == "" { return nil } // Try multiple formats SQLite may return. for _, layout := range []string{ "2006-01-02 15:04:05", "2006-01-02T15:04:05Z", time.RFC3339, } { if t, err := time.Parse(layout, ns.String); err == nil { return &t } } return nil }