@@ -238,16 +238,28 @@ func (p *PostgresDB) InitSchema() error {
238238 strategy TEXT NOT NULL,
239239 pattern TEXT,
240240 is_running BOOLEAN DEFAULT FALSE,
241+ last_hash TEXT,
242+ last_run_at TIMESTAMP,
243+ change_count INTEGER DEFAULT 0,
241244 created_at TIMESTAMP DEFAULT NOW(),
242245 updated_at TIMESTAMP DEFAULT NOW()
243246 );
244247
245- -- Ensure is_running column exists (for backward compatibility)
248+ -- Ensure new columns exist (for backward compatibility)
246249 DO $$
247250 BEGIN
248251 IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='updates_targets' AND column_name='is_running') THEN
249252 ALTER TABLE updates_targets ADD COLUMN is_running BOOLEAN DEFAULT FALSE;
250253 END IF;
254+ IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='updates_targets' AND column_name='last_hash') THEN
255+ ALTER TABLE updates_targets ADD COLUMN last_hash TEXT;
256+ END IF;
257+ IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='updates_targets' AND column_name='last_run_at') THEN
258+ ALTER TABLE updates_targets ADD COLUMN last_run_at TIMESTAMP;
259+ END IF;
260+ IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='updates_targets' AND column_name='change_count') THEN
261+ ALTER TABLE updates_targets ADD COLUMN change_count INTEGER DEFAULT 0;
262+ END IF;
251263 END $$;
252264
253265 -- Create subdomain_monitor_targets table for subdomain monitoring
@@ -258,10 +270,34 @@ func (p *PostgresDB) InitSchema() error {
258270 threads INTEGER DEFAULT 100,
259271 check_new BOOLEAN DEFAULT TRUE,
260272 is_running BOOLEAN DEFAULT FALSE,
273+ last_run_at TIMESTAMP,
261274 created_at TIMESTAMP DEFAULT NOW(),
262275 updated_at TIMESTAMP DEFAULT NOW()
263276 );
264277
278+ -- Ensure last_run_at exists (for backward compatibility)
279+ DO $$
280+ BEGIN
281+ IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='subdomain_monitor_targets' AND column_name='last_run_at') THEN
282+ ALTER TABLE subdomain_monitor_targets ADD COLUMN last_run_at TIMESTAMP;
283+ END IF;
284+ END $$;
285+
286+ -- Create monitor_changes table for change history
287+ CREATE TABLE IF NOT EXISTS monitor_changes (
288+ id SERIAL PRIMARY KEY,
289+ target_type VARCHAR(20) NOT NULL,
290+ target_id INTEGER NOT NULL,
291+ domain TEXT NOT NULL,
292+ change_type VARCHAR(50) NOT NULL,
293+ detail TEXT,
294+ detected_at TIMESTAMP DEFAULT NOW(),
295+ notified BOOLEAN DEFAULT FALSE
296+ );
297+ CREATE INDEX IF NOT EXISTS idx_monitor_changes_domain ON monitor_changes(domain);
298+ CREATE INDEX IF NOT EXISTS idx_monitor_changes_detected_at ON monitor_changes(detected_at);
299+ CREATE INDEX IF NOT EXISTS idx_monitor_changes_change_type ON monitor_changes(change_type);
300+
265301 -- Create scans table for scan progress tracking
266302 CREATE TABLE IF NOT EXISTS scans (
267303 id SERIAL PRIMARY KEY,
@@ -828,7 +864,7 @@ func (p *PostgresDB) GetMonitorTargetByID(id int) (*MonitorTarget, error) {
828864// ListSubdomainMonitorTargets returns all subdomain monitoring targets
829865func (p * PostgresDB ) ListSubdomainMonitorTargets () ([]SubdomainMonitorTarget , error ) {
830866 rows , err := p .pool .Query (p .ctx , `
831- SELECT id, domain, interval_seconds, threads, check_new, is_running, created_at, updated_at
867+ SELECT id, domain, interval_seconds, threads, check_new, is_running, last_run_at, created_at, updated_at
832868 FROM subdomain_monitor_targets
833869 ORDER BY domain;
834870 ` )
@@ -840,7 +876,7 @@ func (p *PostgresDB) ListSubdomainMonitorTargets() ([]SubdomainMonitorTarget, er
840876 var targets []SubdomainMonitorTarget
841877 for rows .Next () {
842878 var t SubdomainMonitorTarget
843- if err := rows .Scan (& t .ID , & t .Domain , & t .Interval , & t .Threads , & t .CheckNew , & t .IsRunning , & t .CreatedAt , & t .UpdatedAt ); err != nil {
879+ if err := rows .Scan (& t .ID , & t .Domain , & t .Interval , & t .Threads , & t .CheckNew , & t .IsRunning , & t .LastRunAt , & t . CreatedAt , & t .UpdatedAt ); err != nil {
844880 return nil , fmt .Errorf ("failed to scan subdomain monitor target: %v" , err )
845881 }
846882 targets = append (targets , t )
@@ -904,10 +940,10 @@ func (p *PostgresDB) SetSubdomainMonitorRunningStatus(id int, isRunning bool) er
904940func (p * PostgresDB ) GetSubdomainMonitorTargetByID (id int ) (* SubdomainMonitorTarget , error ) {
905941 var t SubdomainMonitorTarget
906942 err := p .pool .QueryRow (p .ctx , `
907- SELECT id, domain, interval_seconds, threads, check_new, is_running, created_at, updated_at
943+ SELECT id, domain, interval_seconds, threads, check_new, is_running, last_run_at, created_at, updated_at
908944 FROM subdomain_monitor_targets
909945 WHERE id = $1;
910- ` , id ).Scan (& t .ID , & t .Domain , & t .Interval , & t .Threads , & t .CheckNew , & t .IsRunning , & t .CreatedAt , & t .UpdatedAt )
946+ ` , id ).Scan (& t .ID , & t .Domain , & t .Interval , & t .Threads , & t .CheckNew , & t .IsRunning , & t .LastRunAt , & t . CreatedAt , & t .UpdatedAt )
911947
912948 if err == pgx .ErrNoRows {
913949 return nil , fmt .Errorf ("subdomain monitor target not found with id: %d" , id )
@@ -918,6 +954,88 @@ func (p *PostgresDB) GetSubdomainMonitorTargetByID(id int) (*SubdomainMonitorTar
918954 return & t , nil
919955}
920956
957+ // UpdateSubdomainMonitorLastRun updates last_run_at to now for a subdomain monitor target
958+ func (p * PostgresDB ) UpdateSubdomainMonitorLastRun (id int ) error {
959+ _ , err := p .pool .Exec (p .ctx , `
960+ UPDATE subdomain_monitor_targets
961+ SET last_run_at = NOW()
962+ WHERE id = $1;
963+ ` , id )
964+ if err != nil {
965+ return fmt .Errorf ("failed to update subdomain monitor last_run_at: %v" , err )
966+ }
967+ return nil
968+ }
969+
970+ // UpdateMonitorTargetLastRun updates last_hash, last_run_at, and optionally increments change_count
971+ func (p * PostgresDB ) UpdateMonitorTargetLastRun (id int , hash string , changed bool ) error {
972+ _ , err := p .pool .Exec (p .ctx , `
973+ UPDATE updates_targets
974+ SET last_hash = $1,
975+ last_run_at = NOW(),
976+ change_count = CASE WHEN $2 THEN change_count + 1 ELSE change_count END
977+ WHERE id = $3;
978+ ` , hash , changed , id )
979+ if err != nil {
980+ return fmt .Errorf ("failed to update monitor target last run: %v" , err )
981+ }
982+ return nil
983+ }
984+
985+ // InsertMonitorChange records a detected change in the monitor_changes table
986+ func (p * PostgresDB ) InsertMonitorChange (change * MonitorChange ) error {
987+ _ , err := p .pool .Exec (p .ctx , `
988+ INSERT INTO monitor_changes (target_type, target_id, domain, change_type, detail, notified)
989+ VALUES ($1, $2, $3, $4, $5, $6);
990+ ` , change .TargetType , change .TargetID , change .Domain , change .ChangeType , change .Detail , change .Notified )
991+ if err != nil {
992+ return fmt .Errorf ("failed to insert monitor change: %v" , err )
993+ }
994+ return nil
995+ }
996+
997+ // ListMonitorChanges lists recent monitor changes, optionally filtered by domain
998+ func (p * PostgresDB ) ListMonitorChanges (domain string , limit int ) ([]MonitorChange , error ) {
999+ if limit <= 0 {
1000+ limit = 100
1001+ }
1002+ var rows interface { Close () }
1003+ var err error
1004+ var query string
1005+ var args []interface {}
1006+
1007+ if domain != "" {
1008+ query = `SELECT id, target_type, target_id, domain, change_type, COALESCE(detail,'') as detail, detected_at, notified
1009+ FROM monitor_changes WHERE domain = $1 ORDER BY detected_at DESC LIMIT $2;`
1010+ args = []interface {}{domain , limit }
1011+ } else {
1012+ query = `SELECT id, target_type, target_id, domain, change_type, COALESCE(detail,'') as detail, detected_at, notified
1013+ FROM monitor_changes ORDER BY detected_at DESC LIMIT $1;`
1014+ args = []interface {}{limit }
1015+ }
1016+
1017+ pgRows , pgErr := p .pool .Query (p .ctx , query , args ... )
1018+ if pgErr != nil {
1019+ return nil , fmt .Errorf ("failed to query monitor changes: %v" , pgErr )
1020+ }
1021+ rows = pgRows
1022+ defer pgRows .Close ()
1023+
1024+ var changes []MonitorChange
1025+ for pgRows .Next () {
1026+ var c MonitorChange
1027+ if err = pgRows .Scan (& c .ID , & c .TargetType , & c .TargetID , & c .Domain , & c .ChangeType , & c .Detail , & c .DetectedAt , & c .Notified ); err != nil {
1028+ return nil , fmt .Errorf ("failed to scan monitor change: %v" , err )
1029+ }
1030+ changes = append (changes , c )
1031+ }
1032+ _ = rows
1033+ if pgRows .Err () != nil {
1034+ return nil , fmt .Errorf ("failed to iterate monitor changes: %v" , pgRows .Err ())
1035+ }
1036+ return changes , nil
1037+ }
1038+
9211039// CreateScan creates a new scan record
9221040func (p * PostgresDB ) CreateScan (scan * ScanRecord ) error {
9231041 completedPhasesJSON := "[]"
0 commit comments