feat: add POST /sync endpoint + daily n8n drift check workflow

Adds extension sync that queries each customer's EspoCRM to compare
actually-installed extensions with what the platform DB tracks.
Updates platform DB on version mismatches and reports drifts to Mattermost.

n8n workflow ID: NXxBHm3qhWL3yHza (daily at 07:00)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-29 21:26:01 +00:00
parent f028299693
commit 36812af6e7
2 changed files with 260 additions and 0 deletions
+169
View File
@@ -0,0 +1,169 @@
{
"name": "Extension Sync: Daily Drift Check",
"nodes": [
{
"parameters": {
"rule": {
"interval": [
{
"triggerAtHour": 7
}
]
}
},
"id": "schedule-trigger",
"name": "Daily 07:00",
"type": "n8n-nodes-base.scheduleTrigger",
"typeVersion": 1.2,
"position": [0, 0]
},
{
"parameters": {
"method": "POST",
"url": "https://platform.dev.marcus-law.co.il/sync",
"sendBody": true,
"bodyParameters": {
"parameters": []
},
"options": {
"timeout": 120000
}
},
"id": "call-sync-api",
"name": "Call Platform Sync",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [220, 0]
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict"
},
"conditions": [
{
"id": "drift-check",
"leftValue": "={{ $json.total_drifts }}",
"rightValue": 0,
"operator": {
"type": "number",
"operation": "gt"
}
}
],
"combinator": "and"
},
"options": {}
},
"id": "check-drifts",
"name": "Has Drifts?",
"type": "n8n-nodes-base.if",
"typeVersion": 2,
"position": [440, 0]
},
{
"parameters": {
"jsCode": "// Build Mattermost message from sync results\nconst syncResult = $input.first().json;\nconst results = syncResult.results || [];\n\nlet lines = [\n `### :warning: Extension Drift Report`,\n `**${syncResult.customers_checked}** customers checked | **${syncResult.total_drifts}** drifts found`,\n ''\n];\n\nfor (const r of results) {\n if (r.status === 'error') {\n lines.push(`#### :x: ${r.customer}`);\n lines.push(`> ${r.message}`);\n lines.push('');\n continue;\n }\n if (!r.drifts || r.drifts.length === 0) continue;\n\n lines.push(`#### :mag: ${r.customer} (${r.drift_count} drifts)`);\n lines.push(`| Extension | Type | Platform | Server |`);\n lines.push(`|-----------|------|----------|--------|`);\n\n for (const d of r.drifts) {\n const type = {\n 'version_mismatch': ':arrows_counterclockwise: Version mismatch',\n 'missing_on_server': ':red_circle: Missing on server',\n 'extra_on_server': ':large_blue_circle: Extra on server'\n }[d.type] || d.type;\n lines.push(`| ${d.extension} | ${type} | ${d.platform || '-'} | ${d.server || '-'} |`);\n }\n\n if (r.updated > 0) {\n lines.push(`> :white_check_mark: Updated ${r.updated} records in platform DB`);\n }\n lines.push('');\n}\n\nreturn [{ json: { message: lines.join('\\n') } }];"
},
"id": "format-drift-message",
"name": "Format Drift Report",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [660, -100]
},
{
"parameters": {
"jsCode": "// Build success summary\nconst syncResult = $input.first().json;\n\nconst message = [\n `### :white_check_mark: Extension Sync — All Clear`,\n `**${syncResult.customers_checked}** customers checked, no drifts found.`,\n '',\n `| Customer | Server | Platform |`,\n `|----------|--------|----------|`\n];\n\nfor (const r of (syncResult.results || [])) {\n if (r.status === 'error') {\n message.push(`| ${r.customer} | :x: Error | - |`);\n } else {\n message.push(`| ${r.customer} | ${r.server_count} exts | ${r.platform_count} tracked |`);\n }\n}\n\nreturn [{ json: { message: message.join('\\n') } }];"
},
"id": "format-ok-message",
"name": "Format OK Summary",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [660, 100]
},
{
"parameters": {
"method": "POST",
"url": "https://mattermost.dev.marcus-law.co.il/api/v4/posts",
"authentication": "genericCredentialType",
"genericAuthType": "httpHeaderAuth",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ channel_id: '5x1mzsm7djdz7ydqkde93dkq4e', message: $json.message }) }}",
"options": {}
},
"id": "mattermost-drift",
"name": "Mattermost (Drift)",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [880, -100],
"credentials": {
"httpHeaderAuth": {
"id": "mattermost-token",
"name": "Mattermost Token"
}
}
},
{
"parameters": {
"method": "POST",
"url": "https://mattermost.dev.marcus-law.co.il/api/v4/posts",
"authentication": "genericCredentialType",
"genericAuthType": "httpHeaderAuth",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ channel_id: '5x1mzsm7djdz7ydqkde93dkq4e', message: $json.message }) }}",
"options": {}
},
"id": "mattermost-ok",
"name": "Mattermost (OK)",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [880, 100],
"credentials": {
"httpHeaderAuth": {
"id": "mattermost-token",
"name": "Mattermost Token"
}
}
}
],
"connections": {
"Daily 07:00": {
"main": [
[{ "node": "Call Platform Sync", "type": "main", "index": 0 }]
]
},
"Call Platform Sync": {
"main": [
[{ "node": "Has Drifts?", "type": "main", "index": 0 }]
]
},
"Has Drifts?": {
"main": [
[{ "node": "Format Drift Report", "type": "main", "index": 0 }],
[{ "node": "Format OK Summary", "type": "main", "index": 0 }]
]
},
"Format Drift Report": {
"main": [
[{ "node": "Mattermost (Drift)", "type": "main", "index": 0 }]
]
},
"Format OK Summary": {
"main": [
[{ "node": "Mattermost (OK)", "type": "main", "index": 0 }]
]
}
},
"settings": {
"executionOrder": "v1"
},
"tags": [
{ "name": "extension-platform" },
{ "name": "daily" }
]
}