Skip to content

Commit 6f094b6

Browse files
h0tak88rclaude
andcommitted
fix(programs): trigger scope-watch immediately when user searches a program
Closes the gap where Ryan-BBP's new scope wasn't alerting on Discord even after the user could see it in HackerOne: the warmer was rate-limiting on that program, so its cached row stayed stale, and the passive watch only fires on warmer refresh — so a stale row that never gets re-fetched can't trigger an alert. Fix: when the Programs page on-search debounce force-fetches a program's scope (POST /api/scope/program-summaries with force=true), the server now also feeds the fresh summary into ProgramWatchCheckProgram(p). If its latest_target_updated_at is newer than the watermark, Discord alert fires immediately and the watermark advances (so the next warmer refresh doesn't re-alert on the same update). Net effect: searching "ryan-bbp" in the Programs page = wake the watch up for it. Force-fetches reach H1 directly (no internal 15-min cache to keep masking the new scope), so a real new asset shows up in Discord within seconds of the search. Bypasses the boot intro and per-cycle cap so a single force-fetch can fire exactly one alert per program. No-op when watch disabled or no watermark yet (lets the warmer establish baseline first). Verified: go build/vet clean. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 8d6f824 commit 6f094b6

2 files changed

Lines changed: 36 additions & 0 deletions

File tree

internal/api/program_watch.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,35 @@ func ProgramWatchOnRefresh(programs []ProgramSummary) {
142142
_ = db.SetSetting(programWatchWatermarkKey, freshest.LatestTargetUpdatedAt)
143143
}
144144

145+
// ProgramWatchCheckProgram fires the watch for a SINGLE program — used when a
146+
// force-refreshed scope summary comes back (e.g. user searched the Programs page
147+
// for "ryan-bbp"). If the program's latest_target_updated_at is newer than the
148+
// persisted watermark, alert immediately and advance the watermark so the next
149+
// warmer refresh doesn't re-alert. No-op when watch is disabled or the program
150+
// can't be checked. Bypasses the boot intro and the per-cycle cap.
151+
func ProgramWatchCheckProgram(p ProgramSummary) {
152+
if !programWatchEnabled() || !programWatchPlatformAllowed(p.Platform) {
153+
return
154+
}
155+
if p.LatestTargetUpdatedAt == "" {
156+
return
157+
}
158+
programWatchMu.Lock()
159+
defer programWatchMu.Unlock()
160+
watermark, _ := db.GetSetting(programWatchWatermarkKey)
161+
if watermark == "" {
162+
// No baseline yet — let the regular refresh path establish it silently
163+
// (otherwise the first search after a fresh install would alert on
164+
// whatever happened to be cached).
165+
return
166+
}
167+
if !isNewerProgramTime(p.LatestTargetUpdatedAt, watermark) {
168+
return
169+
}
170+
sendProgramUpdateDiscord(p)
171+
_ = db.SetSetting(programWatchWatermarkKey, p.LatestTargetUpdatedAt)
172+
}
173+
145174
func sendProgramIntroDiscord(p ProgramSummary) {
146175
msg := fmt.Sprintf("📌 **Scope watch ready** — most recent update: **%s** (`%s` · %s)\n• Latest asset: `%s`\n• Updated: %s",
147176
programWatchName(p), p.Handle, strings.ToUpper(p.Platform),

internal/api/programs_api.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,13 @@ func apiProgramScopeSummaries(c *gin.Context) {
307307
// hide the program's real scope until the TTL expires.
308308
if fetched {
309309
setCachedProgramScope(cacheKey, summary)
310+
// User-initiated force-fetch (Programs page search) → also feed the
311+
// scope-update watch so a genuinely-newer-than-watermark program
312+
// alerts Discord immediately, instead of waiting for the next
313+
// warmer refresh that might re-rate-limit it.
314+
if req.Force {
315+
ProgramWatchCheckProgram(summary)
316+
}
310317
}
311318
mu.Lock()
312319
summaries[cacheKey] = summary

0 commit comments

Comments
 (0)