Expand AI auto comment rate limiting

This commit is contained in:
2026-04-07 15:55:04 +02:00
parent 9ca9233bf6
commit b5b44d1304
3 changed files with 812 additions and 214 deletions

View File

@@ -1146,11 +1146,69 @@
</div>
<div class="form-group">
<label class="form-label">Tageslimit pro Profil für den AI-Kommentar-Button</label>
<label class="form-label">
<input type="checkbox" id="aiAutoCommentRateLimitsEnabled" class="form-checkbox">
<span>Limitschutz für den AI-Kommentar-Button aktivieren</span>
</label>
<p class="form-help">
Gilt nur für die Aktion <code>AI - generiere automatisch einen passenden Kommentar</code> im Tracker. <code>0</code> bedeutet kein Limit.
Gilt nur für die Aktion <code>AI - generiere automatisch einen passenden Kommentar</code> im Tracker. Gezählt wird separat je Profil, die Regeln gelten aber für alle Profile gleich.
</p>
<div id="aiProfileCommentLimits"></div>
</div>
<div class="form-group">
<label for="aiRequestsPerMinute" class="form-label">Max. Aktionen pro Minute</label>
<input type="number" id="aiRequestsPerMinute" class="form-input" min="0" max="60" step="1">
<p class="form-help"><code>0</code> deaktiviert dieses Teillimit.</p>
</div>
<div class="form-group">
<label for="aiRequestsPerHour" class="form-label">Max. Aktionen pro Stunde</label>
<input type="number" id="aiRequestsPerHour" class="form-input" min="0" max="500" step="1">
<p class="form-help"><code>0</code> deaktiviert dieses Teillimit.</p>
</div>
<div class="form-group">
<label for="aiRequestsPerDay" class="form-label">Max. Aktionen pro Tag</label>
<input type="number" id="aiRequestsPerDay" class="form-input" min="0" max="5000" step="1">
<p class="form-help"><code>0</code> deaktiviert dieses Teillimit.</p>
</div>
<div class="form-group">
<label for="aiMinDelaySeconds" class="form-label">Mindestabstand zwischen Aktionen (Sekunden)</label>
<input type="number" id="aiMinDelaySeconds" class="form-input" min="0" max="3600" step="1">
<p class="form-help"><code>0</code> deaktiviert dieses Teillimit.</p>
</div>
<div class="form-group">
<label for="aiBurstLimit" class="form-label">Burst-Limit für kurze Spitzen</label>
<input type="number" id="aiBurstLimit" class="form-input" min="0" max="100" step="1">
<p class="form-help">Maximale Aktionen in 10 Minuten. <code>0</code> deaktiviert dieses Teillimit.</p>
</div>
<div class="form-group">
<label for="aiCooldownMinutes" class="form-label">Cooldown nach 429/403/Warnsignal (Minuten)</label>
<input type="number" id="aiCooldownMinutes" class="form-input" min="0" max="1440" step="1">
<p class="form-help"><code>0</code> deaktiviert den zusätzlichen Cooldown.</p>
</div>
<div class="form-group">
<label class="form-label">Aktivzeiten optional</label>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 12px;">
<div>
<label for="aiActiveHoursStart" class="form-label">Von</label>
<input type="time" id="aiActiveHoursStart" class="form-input">
</div>
<div>
<label for="aiActiveHoursEnd" class="form-label">Bis</label>
<input type="time" id="aiActiveHoursEnd" class="form-input">
</div>
</div>
<p class="form-help">Leer lassen für 24/7. Zeiten gelten täglich und dürfen über Mitternacht laufen.</p>
</div>
<div class="form-group">
<label class="form-label">Aktueller Status je Profil</label>
<div id="aiRateLimitProfileStatuses"></div>
</div>
<div class="form-actions">

View File

@@ -41,7 +41,17 @@ const PROVIDER_INFO = {
apiKeyHelp: 'Für lokale OpenAI-kompatible Server (z.B. Ollama) kannst du den Schlüssel leer lassen.'
}
};
const AI_AUTO_COMMENT_DAILY_LIMIT_MAX = 500;
const AI_AUTO_COMMENT_RATE_LIMIT_DEFAULTS = Object.freeze({
enabled: 1,
requests_per_minute: 2,
requests_per_hour: 20,
requests_per_day: 80,
min_delay_seconds: 45,
burst_limit: 5,
cooldown_minutes: 15,
active_hours_start: '',
active_hours_end: ''
});
let credentials = [];
let currentSettings = null;
@@ -174,7 +184,8 @@ async function loadSettings() {
document.getElementById('activeCredential').value = currentSettings.active_credential_id || '';
document.getElementById('aiPromptPrefix').value = currentSettings.prompt_prefix ||
'Schreibe einen freundlichen, authentischen Kommentar auf Deutsch zu folgendem Facebook-Post. Der Kommentar soll natürlich wirken und maximal 2-3 Sätze lang sein:\n\n';
renderAIProfileLimitInputs();
applyAIAutoCommentRateLimitSettingsUI();
renderAIAutoCommentRateLimitStatuses();
}
async function loadHiddenSettings() {
@@ -427,91 +438,169 @@ function normalizeSimilarityImageThresholdInput(value) {
return Math.min(64, Math.max(0, parsed));
}
function normalizeAIProfileDailyLimitInput(value) {
function normalizeAIAutoCommentRateLimitValue(value, fallback, max) {
const parsed = parseInt(value, 10);
if (Number.isNaN(parsed) || parsed < 0) {
return 0;
return fallback;
}
return Math.min(AI_AUTO_COMMENT_DAILY_LIMIT_MAX, parsed);
return Math.min(max, parsed);
}
function getDefaultAIProfileLimitEntry(profileNumber) {
function normalizeAIAutoCommentTimeInput(value) {
if (!value || typeof value !== 'string') {
return '';
}
const trimmed = value.trim();
if (!trimmed) {
return '';
}
return /^\d{2}:\d{2}$/.test(trimmed) ? trimmed : '';
}
function getAIAutoCommentRateLimitSettings() {
const raw = currentSettings?.rate_limit_settings || {};
return {
profile_number: profileNumber,
profile_name: `Profil ${profileNumber}`,
daily_limit: 0,
used_today: 0,
remaining_today: null
enabled: raw.enabled === undefined || raw.enabled === null
? AI_AUTO_COMMENT_RATE_LIMIT_DEFAULTS.enabled
: (raw.enabled ? 1 : 0),
requests_per_minute: normalizeAIAutoCommentRateLimitValue(raw.requests_per_minute, AI_AUTO_COMMENT_RATE_LIMIT_DEFAULTS.requests_per_minute, 60),
requests_per_hour: normalizeAIAutoCommentRateLimitValue(raw.requests_per_hour, AI_AUTO_COMMENT_RATE_LIMIT_DEFAULTS.requests_per_hour, 500),
requests_per_day: normalizeAIAutoCommentRateLimitValue(raw.requests_per_day, AI_AUTO_COMMENT_RATE_LIMIT_DEFAULTS.requests_per_day, 5000),
min_delay_seconds: normalizeAIAutoCommentRateLimitValue(raw.min_delay_seconds, AI_AUTO_COMMENT_RATE_LIMIT_DEFAULTS.min_delay_seconds, 3600),
burst_limit: normalizeAIAutoCommentRateLimitValue(raw.burst_limit, AI_AUTO_COMMENT_RATE_LIMIT_DEFAULTS.burst_limit, 100),
cooldown_minutes: normalizeAIAutoCommentRateLimitValue(raw.cooldown_minutes, AI_AUTO_COMMENT_RATE_LIMIT_DEFAULTS.cooldown_minutes, 1440),
active_hours_start: normalizeAIAutoCommentTimeInput(raw.active_hours_start),
active_hours_end: normalizeAIAutoCommentTimeInput(raw.active_hours_end)
};
}
function getAIProfileLimitEntries() {
const rows = Array.isArray(currentSettings?.profile_limits)
? currentSettings.profile_limits
: [];
const byProfile = new Map();
rows.forEach((row) => {
const profileNumber = parseInt(row?.profile_number, 10);
if (!Number.isNaN(profileNumber) && profileNumber >= 1 && profileNumber <= 5) {
byProfile.set(profileNumber, {
...getDefaultAIProfileLimitEntry(profileNumber),
...row,
daily_limit: normalizeAIProfileDailyLimitInput(row?.daily_limit),
used_today: Math.max(0, parseInt(row?.used_today, 10) || 0)
});
}
});
function applyAIAutoCommentRateLimitSettingsUI() {
const settings = getAIAutoCommentRateLimitSettings();
const toggle = document.getElementById('aiAutoCommentRateLimitsEnabled');
const requestsPerMinute = document.getElementById('aiRequestsPerMinute');
const requestsPerHour = document.getElementById('aiRequestsPerHour');
const requestsPerDay = document.getElementById('aiRequestsPerDay');
const minDelaySeconds = document.getElementById('aiMinDelaySeconds');
const burstLimit = document.getElementById('aiBurstLimit');
const cooldownMinutes = document.getElementById('aiCooldownMinutes');
const activeHoursStart = document.getElementById('aiActiveHoursStart');
const activeHoursEnd = document.getElementById('aiActiveHoursEnd');
return Array.from({ length: 5 }, (_unused, index) => {
const profileNumber = index + 1;
return byProfile.get(profileNumber) || getDefaultAIProfileLimitEntry(profileNumber);
if (toggle) toggle.checked = !!settings.enabled;
if (requestsPerMinute) requestsPerMinute.value = settings.requests_per_minute;
if (requestsPerHour) requestsPerHour.value = settings.requests_per_hour;
if (requestsPerDay) requestsPerDay.value = settings.requests_per_day;
if (minDelaySeconds) minDelaySeconds.value = settings.min_delay_seconds;
if (burstLimit) burstLimit.value = settings.burst_limit;
if (cooldownMinutes) cooldownMinutes.value = settings.cooldown_minutes;
if (activeHoursStart) activeHoursStart.value = settings.active_hours_start || '';
if (activeHoursEnd) activeHoursEnd.value = settings.active_hours_end || '';
applyAIAutoCommentRateLimitEnabledState(!!settings.enabled);
}
function applyAIAutoCommentRateLimitEnabledState(enabled) {
[
'aiRequestsPerMinute',
'aiRequestsPerHour',
'aiRequestsPerDay',
'aiMinDelaySeconds',
'aiBurstLimit',
'aiCooldownMinutes',
'aiActiveHoursStart',
'aiActiveHoursEnd'
].forEach((id) => {
const input = document.getElementById(id);
if (input) {
input.disabled = !enabled;
}
});
}
function renderAIProfileLimitInputs() {
const container = document.getElementById('aiProfileCommentLimits');
function renderAIAutoCommentRateLimitStatuses() {
const container = document.getElementById('aiRateLimitProfileStatuses');
if (!container) {
return;
}
const entries = getAIProfileLimitEntries();
const statuses = Array.isArray(currentSettings?.rate_limit_statuses)
? currentSettings.rate_limit_statuses
: [];
container.innerHTML = '';
entries.forEach((entry) => {
const wrapper = document.createElement('div');
wrapper.className = 'form-group';
wrapper.style.marginBottom = '12px';
if (!statuses.length) {
const empty = document.createElement('p');
empty.className = 'form-help';
empty.textContent = 'Noch keine Statusdaten verfügbar.';
container.appendChild(empty);
return;
}
const label = document.createElement('label');
label.className = 'form-label';
label.setAttribute('for', `aiProfileLimit${entry.profile_number}`);
label.textContent = entry.profile_name || `Profil ${entry.profile_number}`;
statuses.forEach((status) => {
const card = document.createElement('div');
card.className = 'form-group';
card.style.marginBottom = '12px';
card.style.padding = '12px';
card.style.border = '1px solid #dfe3e8';
card.style.borderRadius = '8px';
card.style.background = status.blocked ? '#fff7f7' : '#f8fafc';
const input = document.createElement('input');
input.type = 'number';
input.min = '0';
input.max = String(AI_AUTO_COMMENT_DAILY_LIMIT_MAX);
input.step = '1';
input.id = `aiProfileLimit${entry.profile_number}`;
input.className = 'form-input';
input.value = String(normalizeAIProfileDailyLimitInput(entry.daily_limit));
input.placeholder = '0 = kein Limit';
input.addEventListener('blur', () => {
input.value = String(normalizeAIProfileDailyLimitInput(input.value));
});
const title = document.createElement('div');
title.className = 'form-label';
title.textContent = status.profile_name || `Profil ${status.profile_number}`;
card.appendChild(title);
const help = document.createElement('p');
help.className = 'form-help';
const dailyLimit = normalizeAIProfileDailyLimitInput(entry.daily_limit);
const usedToday = Math.max(0, parseInt(entry.used_today, 10) || 0);
help.textContent = dailyLimit > 0
? `Heute verwendet: ${usedToday} / ${dailyLimit}`
: `Heute verwendet: ${usedToday} · aktuell kein Limit aktiv`;
const summary = document.createElement('p');
summary.className = 'form-help';
const usage = status.usage || {};
const remaining = status.remaining || {};
summary.textContent = `Minute ${usage.minute ?? 0}${remaining.minute !== null && remaining.minute !== undefined ? ` (${remaining.minute} frei)` : ''} · Stunde ${usage.hour ?? 0}${remaining.hour !== null && remaining.hour !== undefined ? ` (${remaining.hour} frei)` : ''} · Tag ${usage.day ?? 0}${remaining.day !== null && remaining.day !== undefined ? ` (${remaining.day} frei)` : ''} · Burst ${usage.burst ?? 0}${remaining.burst !== null && remaining.burst !== undefined ? ` (${remaining.burst} frei)` : ''}`;
card.appendChild(summary);
wrapper.appendChild(label);
wrapper.appendChild(input);
wrapper.appendChild(help);
container.appendChild(wrapper);
const details = document.createElement('p');
details.className = 'form-help';
const detailParts = [];
if (status.last_action_at) {
detailParts.push(`Letzte Aktion ${formatRelativePast(status.last_action_at)}`);
}
if (status.active_hours?.configured) {
detailParts.push(`Aktivzeit ${status.active_hours.start}${status.active_hours.end}`);
} else {
detailParts.push('Aktivzeit 24/7');
}
if (status.cooldown_until) {
detailParts.push(`Cooldown bis ${formatRelativeFuture(status.cooldown_until)}`);
}
details.textContent = detailParts.join(' · ');
card.appendChild(details);
const state = document.createElement('p');
state.className = 'form-help';
state.style.marginBottom = '0';
if (status.blocked) {
const blockedReasonMap = {
cooldown: 'Cooldown aktiv',
active_hours: 'außerhalb der Aktivzeiten',
min_delay: 'Mindestabstand noch nicht erreicht',
per_minute: 'Minutenlimit erreicht',
burst: 'Burst-Limit erreicht',
per_hour: 'Stundenlimit erreicht',
per_day: 'Tageslimit erreicht'
};
const untilText = status.blocked_until ? ` bis ${formatRelativeFuture(status.blocked_until)}` : '';
state.textContent = `Gesperrt: ${blockedReasonMap[status.blocked_reason] || 'Limit aktiv'}${untilText}`;
state.style.color = '#b42318';
if (status.cooldown_reason) {
state.title = status.cooldown_reason;
}
} else {
state.textContent = 'Aktuell freigegeben';
state.style.color = '#027a48';
}
card.appendChild(state);
container.appendChild(card);
});
}
@@ -1089,14 +1178,17 @@ async function saveSettings(e, { silent = false } = {}) {
enabled: document.getElementById('aiEnabled').checked,
active_credential_id: parseInt(document.getElementById('activeCredential').value) || null,
prompt_prefix: document.getElementById('aiPromptPrefix').value,
profile_limits: Array.from({ length: 5 }, (_unused, index) => {
const profileNumber = index + 1;
const input = document.getElementById(`aiProfileLimit${profileNumber}`);
return {
profile_number: profileNumber,
daily_limit: normalizeAIProfileDailyLimitInput(input ? input.value : 0)
};
})
rate_limit_settings: {
enabled: document.getElementById('aiAutoCommentRateLimitsEnabled').checked ? 1 : 0,
requests_per_minute: normalizeAIAutoCommentRateLimitValue(document.getElementById('aiRequestsPerMinute').value, AI_AUTO_COMMENT_RATE_LIMIT_DEFAULTS.requests_per_minute, 60),
requests_per_hour: normalizeAIAutoCommentRateLimitValue(document.getElementById('aiRequestsPerHour').value, AI_AUTO_COMMENT_RATE_LIMIT_DEFAULTS.requests_per_hour, 500),
requests_per_day: normalizeAIAutoCommentRateLimitValue(document.getElementById('aiRequestsPerDay').value, AI_AUTO_COMMENT_RATE_LIMIT_DEFAULTS.requests_per_day, 5000),
min_delay_seconds: normalizeAIAutoCommentRateLimitValue(document.getElementById('aiMinDelaySeconds').value, AI_AUTO_COMMENT_RATE_LIMIT_DEFAULTS.min_delay_seconds, 3600),
burst_limit: normalizeAIAutoCommentRateLimitValue(document.getElementById('aiBurstLimit').value, AI_AUTO_COMMENT_RATE_LIMIT_DEFAULTS.burst_limit, 100),
cooldown_minutes: normalizeAIAutoCommentRateLimitValue(document.getElementById('aiCooldownMinutes').value, AI_AUTO_COMMENT_RATE_LIMIT_DEFAULTS.cooldown_minutes, 1440),
active_hours_start: normalizeAIAutoCommentTimeInput(document.getElementById('aiActiveHoursStart').value),
active_hours_end: normalizeAIAutoCommentTimeInput(document.getElementById('aiActiveHoursEnd').value)
}
};
const res = await apiFetch(`${API_URL}/ai-settings`, {
@@ -1111,7 +1203,8 @@ async function saveSettings(e, { silent = false } = {}) {
}
currentSettings = await res.json();
renderAIProfileLimitInputs();
applyAIAutoCommentRateLimitSettingsUI();
renderAIAutoCommentRateLimitStatuses();
if (!silent) {
showSuccess('✅ Einstellungen erfolgreich gespeichert');
}
@@ -1343,6 +1436,38 @@ document.getElementById('generateTestComment').addEventListener('click', generat
document.getElementById('purgeHiddenNowBtn').addEventListener('click', purgeHiddenNow);
document.getElementById('saveAllFloatingBtn').addEventListener('click', saveAllSettings);
const aiAutoCommentRateLimitsEnabled = document.getElementById('aiAutoCommentRateLimitsEnabled');
if (aiAutoCommentRateLimitsEnabled) {
aiAutoCommentRateLimitsEnabled.addEventListener('change', () => {
applyAIAutoCommentRateLimitEnabledState(aiAutoCommentRateLimitsEnabled.checked);
});
}
[
['aiRequestsPerMinute', AI_AUTO_COMMENT_RATE_LIMIT_DEFAULTS.requests_per_minute, 60],
['aiRequestsPerHour', AI_AUTO_COMMENT_RATE_LIMIT_DEFAULTS.requests_per_hour, 500],
['aiRequestsPerDay', AI_AUTO_COMMENT_RATE_LIMIT_DEFAULTS.requests_per_day, 5000],
['aiMinDelaySeconds', AI_AUTO_COMMENT_RATE_LIMIT_DEFAULTS.min_delay_seconds, 3600],
['aiBurstLimit', AI_AUTO_COMMENT_RATE_LIMIT_DEFAULTS.burst_limit, 100],
['aiCooldownMinutes', AI_AUTO_COMMENT_RATE_LIMIT_DEFAULTS.cooldown_minutes, 1440]
].forEach(([id, fallback, max]) => {
const input = document.getElementById(id);
if (input) {
input.addEventListener('blur', () => {
input.value = normalizeAIAutoCommentRateLimitValue(input.value, fallback, max);
});
}
});
['aiActiveHoursStart', 'aiActiveHoursEnd'].forEach((id) => {
const input = document.getElementById(id);
if (input) {
input.addEventListener('blur', () => {
input.value = normalizeAIAutoCommentTimeInput(input.value);
});
}
});
const autoPurgeHiddenToggle = document.getElementById('autoPurgeHiddenToggle');
if (autoPurgeHiddenToggle) {
autoPurgeHiddenToggle.addEventListener('change', () => {