--- phase: 17-telegram-scheduler plan: 02 type: execute wave: 1 depends_on: [] files_modified: - pkg/scheduler/scheduler.go - pkg/scheduler/jobs.go - pkg/scheduler/scheduler_test.go - pkg/storage/schema.sql - pkg/storage/subscribers.go - pkg/storage/scheduled_jobs.go - go.mod - go.sum autonomous: true requirements: [SCHED-01] must_haves: truths: - "Scheduler loads enabled jobs from SQLite on startup and registers them with gocron" - "Scheduled jobs persist across restarts (stored in scheduled_jobs table)" - "Subscriber chat IDs persist in subscribers table" - "Scheduler executes scan at cron intervals" artifacts: - path: "pkg/scheduler/scheduler.go" provides: "Scheduler struct wrapping gocron with start/stop lifecycle" exports: ["Scheduler", "New", "Start", "Stop"] - path: "pkg/scheduler/jobs.go" provides: "Job struct and CRUD operations" exports: ["Job"] - path: "pkg/storage/scheduled_jobs.go" provides: "SQLite CRUD for scheduled_jobs table" exports: ["ScheduledJob", "SaveScheduledJob", "ListScheduledJobs", "DeleteScheduledJob", "UpdateJobLastRun"] - path: "pkg/storage/subscribers.go" provides: "SQLite CRUD for subscribers table" exports: ["Subscriber", "AddSubscriber", "RemoveSubscriber", "ListSubscribers"] - path: "pkg/storage/schema.sql" provides: "subscribers and scheduled_jobs CREATE TABLE statements" contains: "CREATE TABLE IF NOT EXISTS subscribers" key_links: - from: "pkg/scheduler/scheduler.go" to: "github.com/go-co-op/gocron/v2" via: "gocron.NewScheduler + AddJob" pattern: "gocron\\.NewScheduler" - from: "pkg/scheduler/scheduler.go" to: "pkg/storage" via: "DB.ListScheduledJobs for startup load" pattern: "db\\.ListScheduledJobs" --- Create the pkg/scheduler/ package and the SQLite storage tables (subscribers, scheduled_jobs) that both the bot and scheduler depend on. Purpose: Establishes cron-based recurring scan infrastructure and the persistence layer for subscriptions and jobs. Independent of pkg/bot/ (Wave 1 parallel). Output: pkg/scheduler/, pkg/storage/subscribers.go, pkg/storage/scheduled_jobs.go, updated schema.sql. @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md @.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/17-telegram-scheduler/17-CONTEXT.md @pkg/storage/db.go @pkg/storage/schema.sql @pkg/engine/engine.go From pkg/storage/db.go: ```go type DB struct { sql *sql.DB } func Open(path string) (*DB, error) func (db *DB) Close() error func (db *DB) SQL() *sql.DB ``` From pkg/engine/engine.go: ```go type ScanConfig struct { Workers int; Verify bool; Unmask bool } func (e *Engine) Scan(ctx context.Context, src sources.Source, cfg ScanConfig) (<-chan Finding, error) ``` Task 1: Add gocron dependency, create storage tables, and subscriber/job CRUD go.mod, go.sum, pkg/storage/schema.sql, pkg/storage/subscribers.go, pkg/storage/scheduled_jobs.go 1. Run `go get github.com/go-co-op/gocron/v2@v2.19.1` to add gocron as a direct dependency. 2. Append to pkg/storage/schema.sql (after existing custom_dorks table): ```sql -- Phase 17: Telegram bot subscribers for auto-notifications. CREATE TABLE IF NOT EXISTS subscribers ( chat_id INTEGER PRIMARY KEY, username TEXT, subscribed_at DATETIME DEFAULT CURRENT_TIMESTAMP ); -- Phase 17: Cron-based scheduled scan jobs. CREATE TABLE IF NOT EXISTS scheduled_jobs ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT UNIQUE NOT NULL, cron_expr TEXT NOT NULL, scan_command TEXT NOT NULL, notify_telegram BOOLEAN DEFAULT FALSE, enabled BOOLEAN DEFAULT TRUE, last_run DATETIME, next_run DATETIME, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); ``` 3. Create pkg/storage/subscribers.go: - `Subscriber` struct: `ChatID int64`, `Username string`, `SubscribedAt time.Time` - `(db *DB) AddSubscriber(chatID int64, username string) error` — INSERT OR REPLACE - `(db *DB) RemoveSubscriber(chatID int64) (int64, error)` — DELETE, return rows affected - `(db *DB) ListSubscribers() ([]Subscriber, error)` — SELECT all - `(db *DB) IsSubscribed(chatID int64) (bool, error)` — SELECT count 4. Create pkg/storage/scheduled_jobs.go: - `ScheduledJob` struct: `ID int64`, `Name string`, `CronExpr string`, `ScanCommand string`, `NotifyTelegram bool`, `Enabled bool`, `LastRun *time.Time`, `NextRun *time.Time`, `CreatedAt time.Time` - `(db *DB) SaveScheduledJob(j ScheduledJob) (int64, error)` — INSERT - `(db *DB) ListScheduledJobs() ([]ScheduledJob, error)` — SELECT all - `(db *DB) GetScheduledJob(name string) (*ScheduledJob, error)` — SELECT by name - `(db *DB) DeleteScheduledJob(name string) (int64, error)` — DELETE by name, return rows affected - `(db *DB) UpdateJobLastRun(name string, lastRun time.Time, nextRun *time.Time) error` — UPDATE last_run and next_run - `(db *DB) SetJobEnabled(name string, enabled bool) error` — UPDATE enabled flag cd /home/salva/Documents/apikey && go build ./pkg/storage/... schema.sql has subscribers and scheduled_jobs tables. Storage CRUD methods compile. Task 2: Scheduler package with gocron wrapper and startup job loading pkg/scheduler/scheduler.go, pkg/scheduler/jobs.go, pkg/scheduler/scheduler_test.go - Test 1: SaveScheduledJob + ListScheduledJobs round-trips correctly in :memory: DB - Test 2: AddSubscriber + ListSubscribers round-trips correctly - Test 3: Scheduler.Start loads jobs from DB and registers with gocron - Test 4: Scheduler.AddJob persists to DB and registers cron job - Test 5: Scheduler.RemoveJob removes from DB and gocron 1. Create pkg/scheduler/jobs.go: - `Job` struct mirroring storage.ScheduledJob but with a `RunFunc func(context.Context) (int, error)` field (the scan function to call; returns finding count + error) - `JobResult` struct: `JobName string`, `FindingCount int`, `Duration time.Duration`, `Error error` 2. Create pkg/scheduler/scheduler.go: - `Config` struct: - `DB *storage.DB` - `ScanFunc func(ctx context.Context, scanCommand string) (int, error)` — abstracted scan executor (avoids tight coupling to engine) - `OnComplete func(result JobResult)` — callback for notification bridge (Plan 17-04 wires this) - `Scheduler` struct: - `cfg Config` - `sched gocron.Scheduler` (gocron scheduler instance) - `jobs map[string]gocron.Job` (gocron job handles keyed by name) - `mu sync.Mutex` - `New(cfg Config) (*Scheduler, error)`: - Create gocron scheduler via `gocron.NewScheduler()` - Return Scheduler - `Start(ctx context.Context) error`: - Load all enabled jobs from DB via `cfg.DB.ListScheduledJobs()` - For each, call internal `registerJob(job)` which creates a gocron.CronJob and stores handle - Call `sched.Start()` to begin scheduling - `Stop() error`: - Call `sched.Shutdown()` to stop all jobs - `AddJob(name, cronExpr, scanCommand string, notifyTelegram bool) error`: - Save to DB via `cfg.DB.SaveScheduledJob` - Register with gocron via `registerJob` - `RemoveJob(name string) error`: - Remove gocron job handle from `jobs` map and call `sched.RemoveJob` - Delete from DB via `cfg.DB.DeleteScheduledJob` - `ListJobs() ([]storage.ScheduledJob, error)`: - Delegate to `cfg.DB.ListScheduledJobs()` - `RunJob(ctx context.Context, name string) (JobResult, error)`: - Manual trigger — look up job in DB, call ScanFunc directly, call OnComplete callback - Internal `registerJob(sj storage.ScheduledJob)`: - Create gocron job: `sched.NewJob(gocron.CronJob(sj.CronExpr, false), gocron.NewTask(func() { ... }))` - The task function: call `cfg.ScanFunc(ctx, sj.ScanCommand)`, update last_run/next_run via DB, call `cfg.OnComplete` if sj.NotifyTelegram 3. Create pkg/scheduler/scheduler_test.go: - Use storage.Open(":memory:") for all tests - TestStorageRoundTrip: Save job, list, verify fields match - TestSubscriberRoundTrip: Add subscriber, list, verify; remove, verify empty - TestSchedulerStartLoadsJobs: Save 2 enabled jobs to DB, create Scheduler with mock ScanFunc, call Start, verify gocron has 2 jobs registered (check len(s.jobs)==2) - TestSchedulerAddRemoveJob: Add via Scheduler.AddJob, verify in DB; Remove, verify gone from DB - TestSchedulerRunJob: Manual trigger via RunJob, verify ScanFunc called with correct scanCommand, verify OnComplete called with result cd /home/salva/Documents/apikey && go test ./pkg/scheduler/... ./pkg/storage/... -v -count=1 -run "TestStorage|TestSubscriber|TestScheduler" Scheduler starts, loads jobs from DB, registers with gocron. AddJob/RemoveJob/RunJob work end-to-end. All tests pass. - `go build ./pkg/scheduler/...` compiles without errors - `go test ./pkg/scheduler/... -v` passes all tests - `go test ./pkg/storage/... -v -run Subscriber` passes subscriber CRUD tests - `go test ./pkg/storage/... -v -run ScheduledJob` passes job CRUD tests - `grep gocron go.mod` shows direct dependency at v2.19.1 - pkg/scheduler/ exists with Scheduler struct, gocron wrapper, job loading from DB - pkg/storage/subscribers.go and pkg/storage/scheduled_jobs.go exist with full CRUD - schema.sql has both new tables - gocron v2.19.1 is a direct dependency in go.mod - All tests pass After completion, create `.planning/phases/17-telegram-scheduler/17-02-SUMMARY.md`