{
  "name": "Menu Items Insert Observability",
  "nodes": [
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "menu-items/insert/observability",
        "responseMode": "onReceived",
        "options": {}
      },
      "id": "4ad1d4aa-ef22-4e7e-8c1f-e2ea3509db7e",
      "name": "Menu Items Insert Observability Webhook",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [
        -480,
        0
      ],
      "webhookId": "menu-items-insert-observability"
    },
    {
      "parameters": {
        "jsCode": "const input = $input.first();\nconst body = input.json.body && typeof input.json.body === 'object' ? input.json.body : input.json;\nconst binary = input.binary || {};\n\nfunction parseJson(value) {\n  if (!value || typeof value !== 'string') return value || {};\n  try { return JSON.parse(value); } catch (_) { return { raw: value }; }\n}\n\nfunction escapeHtml(value) {\n  return String(value ?? '')\n    .replace(/&/g, '&amp;')\n    .replace(/</g, '&lt;')\n    .replace(/>/g, '&gt;')\n    .replace(/\\\"/g, '&quot;')\n    .replace(/'/g, '&#39;');\n}\n\nfunction extensionFromMime(mimeType) {\n  if (String(mimeType).includes('png')) return 'png';\n  if (String(mimeType).includes('webp')) return 'webp';\n  if (String(mimeType).includes('gif')) return 'gif';\n  return 'jpg';\n}\n\nconst parsedPayload = parseJson(body.request_payload);\nconst requestPayload = {\n  ...parsedPayload,\n  ...body\n};\ndelete requestPayload.request_payload;\n\nconst workflowVersion = 'observability-public-url-preview-v18';\nconst requestId = requestPayload.request_id || 'unknown';\nconst staticData = $getWorkflowStaticData('global');\nconst nowMs = Date.now();\nstaticData.sentRequestIds = staticData.sentRequestIds || {};\nfor (const [seenRequestId, seenAtMs] of Object.entries(staticData.sentRequestIds)) {\n  if (nowMs - Number(seenAtMs || 0) > 24 * 60 * 60 * 1000) {\n    delete staticData.sentRequestIds[seenRequestId];\n  }\n}\nif (requestId !== 'unknown' && staticData.sentRequestIds[requestId]) {\n  return [];\n}\nif (requestId !== 'unknown') {\n  staticData.sentRequestIds[requestId] = nowMs;\n}\nconst notifyEmail = requestPayload.notify_email || 'darko@appfactory.bg';\nconst vanueName = String(requestPayload.vanueName || '');\nconst uploadedBinary = binary.file || binary.data || Object.values(binary)[0] || null;\nconst mimeType = requestPayload.mime_type || uploadedBinary?.mimeType || 'image/jpeg';\nconst filename = requestPayload.filename || uploadedBinary?.fileName || ('menu-image.' + extensionFromMime(mimeType));\nconst cleanBase64 = String(requestPayload.image_base64 || '')\n  .replace(/^data:image\\/[a-zA-Z0-9.+-]+;base64,/, '')\n  .replace(/\\s/g, '')\n  .trim();\nconst previewBase64 = String(requestPayload.image_preview_base64 || cleanBase64)\n  .replace(/^data:image\\/[a-zA-Z0-9.+-]+;base64,/, '')\n  .replace(/\\s/g, '')\n  .trim();\nconst publicImageUrl = String(requestPayload.received_image_public_url || '').trim();\nconst imageSrc = publicImageUrl || (previewBase64 ? 'data:image/jpeg;base64,' + previewBase64 : '');\nconst previewSource = publicImageUrl ? 'public_url' : (previewBase64 ? 'base64_fallback' : 'none');\n\nlet imageBinary = null;\nlet attachedSource = 'none';\nlet decodedAttachmentBytes = 0;\nif (uploadedBinary) {\n  imageBinary = uploadedBinary;\n  attachedSource = 'webhook_binary';\n} else if (cleanBase64) {\n  imageBinary = {\n    data: cleanBase64,\n    mimeType,\n    fileName: filename,\n  };\n  attachedSource = 'image_base64';\n  decodedAttachmentBytes = Buffer.from(cleanBase64, 'base64').length;\n}\n\nconst outputBinary = imageBinary ? { file: imageBinary } : {};\nconst imageSizeBytes = Number(requestPayload.image_size_bytes || uploadedBinary?.fileSize || decodedAttachmentBytes || 0);\n\nconst emailPayload = { ...requestPayload };\ndelete emailPayload.image_base64;\ndelete emailPayload.image_preview_base64;\n\nconst previewHtml = imageSrc\n  ? '<div style=\"margin:0 0 16px 0\"><img src=\"' + escapeHtml(imageSrc) + '\" alt=\"Uploaded menu scan\" style=\"display:block;max-width:100%;width:640px;height:auto;border:1px solid #ddd\" /></div>'\n  : '<p><strong>Image preview:</strong> unavailable</p>';\n\nconst emailHtml = [\n  '<h2>Menu scan observability</h2>',\n  previewHtml,\n  '<p><strong>Workflow:</strong> ' + escapeHtml(workflowVersion) + '</p>',\n  '<p><strong>Request ID:</strong> ' + escapeHtml(requestId) + '</p>',\n  '<p><strong>City:</strong> ' + escapeHtml(requestPayload.city || 'unknown') + '</p>',\n  '<p><strong>Venue name:</strong> ' + escapeHtml(vanueName) + '</p>',\n  '<p><strong>Zavedenia ID:</strong> ' + escapeHtml(String(requestPayload.zavedenia_id ?? 'unknown')) + '</p>',\n  '<p><strong>Filename:</strong> ' + escapeHtml(filename) + '</p>',\n  '<p><strong>Preview source:</strong> ' + escapeHtml(previewSource) + '</p>',\n  '<p><strong>Public URL:</strong> ' + (publicImageUrl ? '<a href=\"' + escapeHtml(publicImageUrl) + '\">' + escapeHtml(publicImageUrl) + '</a>' : '') + '</p>',\n  '<p><strong>MIME:</strong> ' + escapeHtml(mimeType) + '</p>',\n  '<p><strong>Image bytes:</strong> ' + escapeHtml(String(imageSizeBytes)) + '</p>',\n  '<p><strong>Attachment source:</strong> ' + escapeHtml(attachedSource) + '</p>',\n  '<p><strong>Has attachment:</strong> ' + escapeHtml(String(Boolean(imageBinary))) + '</p>',\n  '<p><strong>SHA256:</strong> ' + escapeHtml(requestPayload.received_image_sha256 || '') + '</p>',\n  '<p><strong>Saved path:</strong> ' + escapeHtml(requestPayload.received_image_saved_path || '') + '</p>',\n].join('');\n\nreturn [{\n  json: {\n    kind: 'menu_items_insert_observability',\n    ok: true,\n    workflow_version: workflowVersion,\n    request_id: requestId,\n    notify_email: notifyEmail,\n    city: requestPayload.city || null,\n    vanueName,\n    zavedenia_id: requestPayload.zavedenia_id || null,\n    filename,\n    mime_type: mimeType,\n    image_size_bytes: imageSizeBytes,\n    has_image_binary: Boolean(imageBinary),\n    has_inline_preview: Boolean(imageSrc),\n    preview_source: previewSource,\n    public_image_url: publicImageUrl,\n    attachment_source: attachedSource,\n    decoded_attachment_bytes: decodedAttachmentBytes,\n    binary_keys: Object.keys(outputBinary),\n    request_payload: emailPayload,\n    email_subject: 'Menu scan observability',\n    email_html: emailHtml\n  },\n  binary: outputBinary\n}];"
      },
      "id": "27104906-3e3e-486c-aa27-035d2e914f3d",
      "name": "Build Observability Email",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -224,
        0
      ]
    },
    {
      "parameters": {
        "fromEmail": "ordermoonmain@gmail.com",
        "toEmail": "zavedenia@gmail.com",
        "subject": "={{ $json.email_subject }}",
        "html": "={{ (() => { const source = String($json.email_html || ''); const preview = source.match(/<div style=\"margin:0 0 16px 0\">.*?<\\/div>/)?.[0] || '<p><strong>Image preview:</strong> unavailable</p>'; const publicUrl = $json.public_image_url ? '<p><strong>Public URL:</strong> <a href=\"' + String($json.public_image_url) + '\">' + String($json.public_image_url) + '</a></p>' : '<p><strong>Public URL:</strong> </p>'; const scanDate = new Intl.DateTimeFormat('en-GB', { timeZone: 'Europe/Skopje', year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit' }).format(new Date()); return '<h2>Menu scan observability</h2><p><strong>Venue name:</strong> ' + String($json.vanueName || '') + '</p><p><strong>Zavedenie ID:</strong> ' + String($json.zavedenia_id ?? '') + '</p><p><strong>City:</strong> ' + String($json.city || '') + '</p><p><strong>Date of scan:</strong> ' + scanDate + '</p>' + preview + publicUrl; })() }}",
        "options": {
          "attachments": "file",
          "appendAttribution": false,
          "ccEmail": "darko@appfactory.bg"
        }
      },
      "id": "e3d6aac7-ecab-45e8-8d8d-c99149a23fb5",
      "name": "Email Insert Upload Observability",
      "type": "n8n-nodes-base.emailSend",
      "typeVersion": 2.1,
      "position": [
        32,
        0
      ],
      "credentials": {
        "smtp": {
          "id": "FHnrd8vBCAukd5VU",
          "name": "SMTP account"
        }
      },
      "continueOnFail": true
    }
  ],
  "pinData": {},
  "connections": {
    "Menu Items Insert Observability Webhook": {
      "main": [
        [
          {
            "node": "Build Observability Email",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Observability Email": {
      "main": [
        [
          {
            "node": "Email Insert Upload Observability",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": true,
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "2b5d70a1-2115-4fdd-91b5-fbb0a6eca0ce",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "id": "menu-items-insert-observability",
  "tags": []
}
