refactor(triggers): review followups — fire-now, dedupe trigger pages, hardening
Build / build (push) Failing after 34s
Build / build (push) Failing after 34s
Follow-ups on commit 39e1e36 addressing review feedback from
go-reviewer / security-reviewer / typescript-reviewer.
Backend:
- New POST /api/triggers/{id}/fire (AdminOnly, schedule-only): operator
"Fire now" button — dispatches immediately without waiting for the
next natural interval. Persists last_fired_at BEFORE dispatch, same
ordering as the scheduler. Per-trigger in-flight guard (429 if a
fire is already running) to defend against rapid double-clicks /
runaway scripts. Refuses request when AdminOnly claims are absent
rather than logging an unattributable deploy.
- SetTriggerLastFired now validates timestamp parses as RFC3339 before
writing. Rejects empty string explicitly — empty-clears semantics
were dead (no caller) and would silently re-fire on next tick if
ever accidentally written. A future reset-cadence flow must add a
dedicated ClearTriggerLastFired so the call site is grep-able and
separately auditable.
- Scheduler logs WARN on catch-up fires (now - lastFired > 2× interval)
so the "surprise burst at restart" pattern shows up in audit logs.
- BindingResult reason strings extracted to package consts
(webhook.Reason*) so the scheduler and api fire-now classifications
stay in sync without string-matching drift.
- SECURITY NOTE on FanOutForTrigger documents that the
WebhookRequireSignature gate is ingress-only by design.
Frontend:
- Refactored /triggers/new (770 LOC → 155 LOC) and /triggers/[id]
(~350 LOC dropped) to use the shared TriggerKindForm. Eliminates the
triplicated per-kind state + buildConfig + canSubmit + template that
caused the d-unit regex drift in the prior commit.
- New seedTriggerKindFormState helper on TriggerKindForm primes the
form from a server-returned trigger config with defensive type
guards; resets per-kind slots first so re-seeding across kinds
doesn't inherit stale state.
- /triggers/[id] gains a Schedule status panel with Last Fired + Fire
Now button (gated on binding_count > 0). Confirmation dialog,
result flash, timer cleanup on unmount + new-fire (no stale-closure
race). EN+RU i18n parity.
This commit is contained in:
@@ -171,9 +171,31 @@ func (s *Scheduler) shouldFire(t store.Trigger, now time.Time) bool {
|
||||
// require a manual DB poke.
|
||||
return true
|
||||
}
|
||||
return !now.Before(last.Add(interval))
|
||||
if now.Before(last.Add(interval)) {
|
||||
return false
|
||||
}
|
||||
// Catch-up warning: a trigger whose last_fired_at is many intervals
|
||||
// old (paused-then-resumed, restored from backup, or just left
|
||||
// running while the dispatcher was down) WILL fire on this tick.
|
||||
// Log a one-line warning so the operator can recognize the "surprise
|
||||
// burst at restart" pattern in audit logs. We still fire — silent
|
||||
// no-fire would be worse — but the warning explains why.
|
||||
if overdue := now.Sub(last); overdue > catchUpWarnThreshold*interval {
|
||||
slog.Warn("scheduler: catch-up fire (very overdue)",
|
||||
"trigger", t.Name, "overdue", overdue, "interval", interval)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// catchUpWarnThreshold is the multiplier on `interval` past which a
|
||||
// fire is logged as "catch-up." 2× means a daily schedule whose last
|
||||
// fire was more than 48h ago gets a warning at next tick. Chosen so
|
||||
// the warning fires on "wedged for many intervals" without alerting on
|
||||
// the every-tick lag a healthy 30s-tick scheduler accumulates against
|
||||
// a sub-minute interval. Bigger threshold = noisier-quiet trade-off;
|
||||
// 2× is the smallest value that excludes single-tick lag.
|
||||
const catchUpWarnThreshold = 2
|
||||
|
||||
// fire dispatches one trigger and records the new last_fired_at.
|
||||
//
|
||||
// We persist last_fired_at BEFORE calling the dispatcher so a panic
|
||||
|
||||
Reference in New Issue
Block a user