Skip to content

Commit de09774

Browse files
accorvinclaude
andauthored
feat: add 5-minute cooldown to field-options sync trigger (#37)
Prevents manual sync triggers from firing more often than every 5 minutes per option set. Returns HTTP 429 with retryAfter when in cooldown. Follows the same pattern used by hygiene, execution, and allocation refresh handlers. Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 84aa51f commit de09774

1 file changed

Lines changed: 16 additions & 0 deletions

File tree

modules/team-tracker/server/index.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2406,6 +2406,8 @@ module.exports = async function registerRoutes(router, context) {
24062406
// ─── Field-Options Sync (Jira link) ───
24072407

24082408
const fieldOptionsSync = require('./field-options-sync');
2409+
const FIELD_OPTIONS_SYNC_COOLDOWN_MS = 5 * 60 * 1000; // 5 minutes
2410+
const _fieldOptionsSyncState = {}; // per-name: { lastSuccessAt: ISO string }
24092411

24102412
/**
24112413
* @openapi
@@ -2734,6 +2736,8 @@ module.exports = async function registerRoutes(router, context) {
27342736
* description: Sync result
27352737
* 400:
27362738
* description: Option set is not linked
2739+
* 429:
2740+
* description: Sync was triggered too recently (cooldown period)
27372741
* 502:
27382742
* description: Sync failed
27392743
*/
@@ -2743,8 +2747,20 @@ module.exports = async function registerRoutes(router, context) {
27432747
}
27442748
const safeName = sanitizeOptionsName(req.params.name);
27452749
if (!safeName) return res.status(400).json({ error: 'Invalid option set name' });
2750+
2751+
// Cooldown: prevent manual triggers more often than every 5 minutes
2752+
const state = _fieldOptionsSyncState[safeName];
2753+
if (state && state.lastSuccessAt) {
2754+
const elapsed = Date.now() - new Date(state.lastSuccessAt).getTime();
2755+
if (elapsed < FIELD_OPTIONS_SYNC_COOLDOWN_MS) {
2756+
const retryAfter = Math.ceil((FIELD_OPTIONS_SYNC_COOLDOWN_MS - elapsed) / 1000);
2757+
return res.status(429).json({ status: 'cooldown', retryAfter });
2758+
}
2759+
}
2760+
27462761
try {
27472762
const result = await fieldOptionsSync.syncOptionSet(storage, jiraRequest, safeName);
2763+
_fieldOptionsSyncState[safeName] = { lastSuccessAt: new Date().toISOString() };
27482764
res.json(result);
27492765
} catch (err) {
27502766
const status = err.message.includes('not linked') ? 400 : 502;

0 commit comments

Comments
 (0)