@@ -15,6 +15,7 @@ import (
1515 "time"
1616
1717 "github.com/gin-gonic/gin"
18+ "github.com/h0tak88r/AutoAR/internal/db"
1819 "github.com/tidwall/gjson"
1920)
2021
@@ -297,7 +298,11 @@ func apiProgramScopeSummaries(c *gin.Context) {
297298 case "bc" , "bugcrowd" :
298299 if bcToken != "" {
299300 summary = fetchBCScopeSummary (item .Handle , item .URL , bcToken )
300- fetched = true
301+ // fetchBCScopeSummary doesn't return ok — all 8 failure paths
302+ // return a zero-valued summary. Treat any empty result as a
303+ // failed fetch so we don't overwrite persisted good data.
304+ // (Matches the enrichBCScopeCounts guard.)
305+ fetched = summary .ScopeTargets > 0 || summary .LatestTarget != ""
301306 }
302307 }
303308
@@ -307,6 +312,13 @@ func apiProgramScopeSummaries(c *gin.Context) {
307312 // hide the program's real scope until the TTL expires.
308313 if fetched {
309314 setCachedProgramScope (cacheKey , summary )
315+ // Persist last-known-good in the DB — so a search-driven refresh
316+ // for a program that the warmer keeps rate-limiting still wins.
317+ _ = db .UpsertProgramScope (db.PersistedProgramScope {
318+ Platform : summary .Platform , Handle : summary .Handle ,
319+ ScopeTargets : summary .ScopeTargets , LatestTarget : summary .LatestTarget ,
320+ LatestTargetUpdatedAt : summary .LatestTargetUpdatedAt , LatestTargetBrief : summary .LatestTargetBrief ,
321+ })
310322 // User-initiated force-fetch (Programs page search) → also feed the
311323 // scope-update watch so a genuinely-newer-than-watermark program
312324 // alerts Discord immediately, instead of waiting for the next
@@ -494,6 +506,13 @@ func enrichH1ScopeCounts(programs []ProgramSummary, auth string) {
494506 p .LatestTarget = summary .LatestTarget
495507 p .LatestTargetUpdatedAt = summary .LatestTargetUpdatedAt
496508 p .LatestTargetBrief = summary .LatestTargetBrief
509+ // Persist last-known-good — failed/rate-limited fetches don't
510+ // reach here, so a transient 429 never overwrites real data.
511+ _ = db .UpsertProgramScope (db.PersistedProgramScope {
512+ Platform : p .Platform , Handle : p .Handle ,
513+ ScopeTargets : p .ScopeTargets , LatestTarget : p .LatestTarget ,
514+ LatestTargetUpdatedAt : p .LatestTargetUpdatedAt , LatestTargetBrief : p .LatestTargetBrief ,
515+ })
497516 }
498517 }(& programs [i ])
499518 }
@@ -679,10 +698,19 @@ func enrichBCScopeCounts(programs []ProgramSummary, token string) {
679698 time .Sleep (500 * time .Millisecond ) // extra spacing for BC
680699
681700 summary := fetchBCScopeSummary (p .Handle , p .URL , token )
682- p .ScopeTargets = summary .ScopeTargets
683- p .LatestTarget = summary .LatestTarget
684- p .LatestTargetUpdatedAt = summary .LatestTargetUpdatedAt
685- p .LatestTargetBrief = summary .LatestTargetBrief
701+ // Only persist (and overwrite the in-memory row) when fetch actually
702+ // returned scope — empty/transient failures leave prior data intact.
703+ if summary .ScopeTargets > 0 || summary .LatestTarget != "" {
704+ p .ScopeTargets = summary .ScopeTargets
705+ p .LatestTarget = summary .LatestTarget
706+ p .LatestTargetUpdatedAt = summary .LatestTargetUpdatedAt
707+ p .LatestTargetBrief = summary .LatestTargetBrief
708+ _ = db .UpsertProgramScope (db.PersistedProgramScope {
709+ Platform : p .Platform , Handle : p .Handle ,
710+ ScopeTargets : p .ScopeTargets , LatestTarget : p .LatestTarget ,
711+ LatestTargetUpdatedAt : p .LatestTargetUpdatedAt , LatestTargetBrief : p .LatestTargetBrief ,
712+ })
713+ }
686714 }(& programs [i ])
687715 }
688716 wg .Wait ()
@@ -916,10 +944,26 @@ func enrichITScopeCounts(programs []ProgramSummary, token string) {
916944 defer func () { <- sem }()
917945
918946 summary := fetchITScopeSummary (client , token , p .ID )
919- p .ScopeTargets = summary .ScopeTargets
920- if p .LatestTarget == "" {
921- p .LatestTarget = summary .LatestTarget
922- p .LatestTargetBrief = summary .LatestTargetBrief
947+ // Only persist on a real scope payload — IT often rate-limits and the
948+ // fetcher swallows that as an empty summary; leaving the in-memory row
949+ // alone preserves whatever was already there (e.g. from the merged DB).
950+ if summary .ScopeTargets > 0 || summary .LatestTarget != "" {
951+ p .ScopeTargets = summary .ScopeTargets
952+ if p .LatestTarget == "" {
953+ p .LatestTarget = summary .LatestTarget
954+ p .LatestTargetBrief = summary .LatestTargetBrief
955+ }
956+ // Overwrite LatestTargetUpdatedAt with the asset-level timestamp from
957+ // fetchITScopeSummary (may be empty if IT didn't expose it). DROP the
958+ // program-level lastUpdatedAt that fetchITPrograms set — otherwise the
959+ // scope-update watch fires on every program edit (bounty bump, etc.)
960+ // not just on real scope changes.
961+ p .LatestTargetUpdatedAt = summary .LatestTargetUpdatedAt
962+ _ = db .UpsertProgramScope (db.PersistedProgramScope {
963+ Platform : p .Platform , Handle : p .Handle ,
964+ ScopeTargets : p .ScopeTargets , LatestTarget : p .LatestTarget ,
965+ LatestTargetUpdatedAt : p .LatestTargetUpdatedAt , LatestTargetBrief : p .LatestTargetBrief ,
966+ })
923967 }
924968 }(& programs [i ])
925969 }
@@ -967,9 +1011,16 @@ func fetchITScopeSummary(client *http.Client, token, programID string) ProgramSu
9671011 if target != "" {
9681012 summary .Assets = append (summary .Assets , target )
9691013 }
970- if target != "" && summary .LatestTarget == "" {
1014+ // Per-asset timestamp (try common IT field names). Use the LATEST one
1015+ // across all assets so the watch fires only when scope genuinely changes
1016+ // — NOT on unrelated program edits (bounty bump, description change, …)
1017+ // which would otherwise look like scope updates if we used the program
1018+ // list's program-level lastUpdatedAt.
1019+ updatedAt := firstGJSONString (v , "updatedAt" , "lastUpdatedAt" , "modifiedAt" , "addedAt" , "createdAt" )
1020+ if target != "" && (summary .LatestTarget == "" || isNewerProgramTime (updatedAt , summary .LatestTargetUpdatedAt )) {
9711021 summary .LatestTarget = target
9721022 summary .LatestTargetBrief = firstGJSONString (v , "description" , "type.value" )
1023+ summary .LatestTargetUpdatedAt = updatedAt
9731024 }
9741025 return true
9751026 })
0 commit comments