fixed n8n webhooks

This commit is contained in:
Purple
2026-01-18 11:40:06 +00:00
parent 012e139af6
commit 72d6f9ae3e
5 changed files with 163 additions and 12 deletions

BIN
.DS_Store vendored

Binary file not shown.

View File

@@ -118,6 +118,10 @@ try {
handleWebhookQueueStatus($db);
break;
case 'webhook_queue_clear':
handleWebhookQueueClear($db);
break;
case 'update_sort_order':
handleUpdateSortOrder($db);
break;
@@ -1173,7 +1177,7 @@ function handleWebhookSettingsGet($db) {
$settings = [
'webhook_url' => getSetting($db, 'n8n_webhook_url', ''),
'webhook_enabled' => getSetting($db, 'n8n_webhook_enabled', '0') === '1',
'webhook_delay_minutes' => intval(getSetting($db, 'n8n_webhook_delay_minutes', '3'))
'webhook_delay_minutes' => intval(getSetting($db, 'n8n_webhook_delay_minutes', '0'))
];
jsonResponse(['success' => true, 'data' => $settings]);
@@ -1196,7 +1200,7 @@ function handleWebhookSettingsSave($db) {
$webhookUrl = trim($input['webhook_url'] ?? '');
$webhookEnabled = !empty($input['webhook_enabled']) ? '1' : '0';
$delayMinutes = max(1, min(60, intval($input['webhook_delay_minutes'] ?? 3)));
$delayMinutes = max(0, min(60, intval($input['webhook_delay_minutes'] ?? 0)));
// Validate URL if provided
if (!empty($webhookUrl) && !filter_var($webhookUrl, FILTER_VALIDATE_URL)) {
@@ -1362,6 +1366,55 @@ function handleWebhookQueueStatus($db) {
]);
}
/**
* Clear webhook queue
*/
function handleWebhookQueueClear($db) {
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
jsonResponse(['error' => 'Method not allowed'], 405);
}
$input = json_decode(file_get_contents('php://input'), true);
// Validate CSRF
if (!validateCSRFToken($input['csrf_token'] ?? '')) {
jsonResponse(['error' => 'Invalid CSRF token'], 403);
}
$clearType = $input['clear_type'] ?? 'pending';
try {
switch ($clearType) {
case 'pending':
$stmt = $db->prepare("DELETE FROM webhook_queue WHERE status IN ('pending', 'processing')");
$stmt->execute();
$message = 'Pending webhooks cleared';
break;
case 'failed':
$stmt = $db->prepare("DELETE FROM webhook_queue WHERE status = 'failed'");
$stmt->execute();
$message = 'Failed webhooks cleared';
break;
case 'all':
$stmt = $db->prepare("DELETE FROM webhook_queue");
$stmt->execute();
$message = 'All webhook history cleared';
break;
default:
jsonResponse(['error' => 'Invalid clear type'], 400);
}
$deletedCount = $stmt->rowCount();
jsonResponse([
'success' => true,
'message' => $message,
'deleted' => $deletedCount
]);
} catch (Exception $e) {
jsonResponse(['error' => 'Failed to clear queue: ' . $e->getMessage()], 500);
}
}
/**
* Update sort order for entries
*/

View File

@@ -171,7 +171,13 @@ function queueWebhookNotification($db, $reason = 'manual', $entriesAffected = 1)
return false;
}
$delayMinutes = intval(getSetting($db, 'n8n_webhook_delay_minutes', '3'));
$delayMinutes = intval(getSetting($db, 'n8n_webhook_delay_minutes', '0'));
// If delay is 0, send immediately without queuing
if ($delayMinutes <= 0) {
return sendWebhookImmediately($db, $webhookUrl, $reason, $entriesAffected);
}
$scheduledFor = date('Y-m-d H:i:s', strtotime("+{$delayMinutes} minutes"));
// Check if there's already a pending webhook scheduled
@@ -198,7 +204,7 @@ function queueWebhookNotification($db, $reason = 'manual', $entriesAffected = 1)
':reason' => $reason,
':id' => $existing['id']
]);
return $existing['id'];
$queueId = $existing['id'];
} else {
// Create new webhook queue entry
$stmt = $db->prepare("
@@ -210,8 +216,56 @@ function queueWebhookNotification($db, $reason = 'manual', $entriesAffected = 1)
':entries' => $entriesAffected,
':scheduled_for' => $scheduledFor
]);
return $db->lastInsertId();
$queueId = $db->lastInsertId();
}
// Also process any due webhooks opportunistically
processWebhookQueue($db);
return $queueId;
}
/**
* Send webhook immediately without queuing (for zero-delay mode)
*/
function sendWebhookImmediately($db, $webhookUrl, $reason, $entriesAffected) {
// Log to queue for history purposes
$stmt = $db->prepare("
INSERT INTO webhook_queue (webhook_type, trigger_reason, entries_affected, scheduled_for, status)
VALUES ('geofeed_update', :reason, :entries, NOW(), 'processing')
");
$stmt->execute([
':reason' => $reason,
':entries' => $entriesAffected
]);
$queueId = $db->lastInsertId();
// Send the webhook immediately
$payload = [
'event' => 'geofeed_update',
'queue_id' => $queueId,
'trigger_reason' => $reason,
'entries_affected' => $entriesAffected,
'timestamp' => date('c')
];
$result = sendWebhook($webhookUrl, $payload);
// Update status
$finalStatus = $result['success'] ? 'completed' : 'failed';
$updateStmt = $db->prepare("
UPDATE webhook_queue
SET status = :status, processed_at = NOW(), response_code = :code, response_body = :body
WHERE id = :id
");
$updateStmt->execute([
':status' => $finalStatus,
':code' => $result['http_code'],
':body' => substr($result['response'] ?? '', 0, 1000),
':id' => $queueId
]);
return $queueId;
}
/**

View File

@@ -784,6 +784,36 @@ function renderWebhookQueueStatus(data) {
container.innerHTML = html;
}
// Clear webhook queue
async function clearWebhookQueue() {
const clearType = document.getElementById('clearQueueType')?.value || 'pending';
const typeLabels = {
'pending': 'pending webhooks',
'failed': 'failed webhooks',
'all': 'all webhook history'
};
if (!confirm(`Are you sure you want to clear ${typeLabels[clearType]}?`)) {
return;
}
try {
const result = await api('webhook_queue_clear', {
csrf_token: CSRF_TOKEN,
clear_type: clearType
});
if (result.success) {
showToast(`${result.message} (${result.deleted} removed)`, 'success');
loadWebhookQueueStatus();
} else {
showToast(result.error || 'Failed to clear queue', 'error');
}
} catch (error) {
showToast('Network error', 'error');
}
}
// Get time until a future date
function getTimeUntil(date) {
const seconds = Math.floor((date - new Date()) / 1000);

View File

@@ -279,13 +279,27 @@ require_once __DIR__ . '/includes/header.php';
<div class="table-container" style="margin-top: 16px;">
<div class="table-header">
<h3 class="table-title">Webhook Queue</h3>
<button class="btn btn-ghost btn-sm" onclick="loadWebhookQueueStatus()">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="23 4 23 10 17 10"/>
<path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/>
</svg>
Refresh
</button>
<div style="display: flex; gap: 8px;">
<button class="btn btn-ghost btn-sm" onclick="loadWebhookQueueStatus()">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="23 4 23 10 17 10"/>
<path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/>
</svg>
Refresh
</button>
<select id="clearQueueType" class="form-select" style="width: auto; padding: 6px 8px; font-size: 12px;">
<option value="pending">Pending</option>
<option value="failed">Failed</option>
<option value="all">All History</option>
</select>
<button class="btn btn-danger btn-sm" onclick="clearWebhookQueue()">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="3 6 5 6 21 6"/>
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>
</svg>
Clear
</button>
</div>
</div>
<div id="webhookQueueContainer" style="padding: 20px;">
<div class="loading"><div class="spinner"></div></div>