AutomationFlowsGeneral › Postagem Redes — Portal Visual

Postagem Redes — Portal Visual

Postagem Redes — Portal Visual. Webhook trigger; 4 nodes.

Webhook trigger★★★★☆ complexity4 nodes
General Trigger: Webhook Nodes: 4 Complexity: ★★★★☆ Added:

The workflow JSON

Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →

Download .json
{
  "name": "Postagem Redes \u2014 Portal Visual",
  "description": null,
  "isArchived": false,
  "nodes": [
    {
      "parameters": {
        "content": "## Portal Visual\n\nEntrega a biblioteca, filtros, modais e pr\u00e9vias para a opera\u00e7\u00e3o di\u00e1ria.\n\n**Sem publica\u00e7\u00e3o externa:** este workflow somente renderiza a interface.",
        "width": 520,
        "height": 175,
        "color": 5
      },
      "name": "Resumo operacional",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -360,
        -290
      ]
    },
    {
      "parameters": {
        "httpMethod": "GET",
        "path": "postagem-redes",
        "responseMode": "responseNode",
        "options": {}
      },
      "name": "Webhook",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2.1,
      "position": [
        -280,
        0
      ]
    },
    {
      "parameters": {
        "jsCode": "\nconst fs = require('fs');\nconst path = require('path');\nconst crypto = require('crypto');\nconst ROOT = '/files/postagem-redes';\nconst INPUT = path.join(ROOT, 'entrada');\nconst STATE = path.join(ROOT, 'state.json');\nconst allowedStatuses = new Set(['pendente','aprovado','agendado','rejeitado','incompleto','publicado','parcial','falhou']);\nfunction slug(value) { const base=String(value).normalize('NFD').replace(/[\\u0300-\\u036f]/g,'').toLowerCase().replace(/[^a-z0-9]+/g,'-').replace(/(^-|-$)/g,'').slice(0,70)||'conteudo'; let h=0; for(const c of String(value)) h=((h<<5)-h+c.charCodeAt(0))|0; return base+'-'+(h>>>0).toString(36); }\nfunction safeText(value,max=5000){return String(value??'').replace(/\\0/g,'').trim().slice(0,max)}\nfunction readJson(file,fallback){try{return JSON.parse(fs.readFileSync(file,'utf8'))}catch{return fallback}}\nfunction saveState(state){const lock=STATE+'.lock'; let fd; try{fd=fs.openSync(lock,'wx')}catch{throw new Error('Outra altera\u00e7\u00e3o est\u00e1 em andamento. Atualize a p\u00e1gina e tente novamente.')} try{const temp=STATE+'.tmp';fs.writeFileSync(temp,JSON.stringify(state,null,2),'utf8');fs.renameSync(temp,STATE)}finally{if(fd!==undefined)fs.closeSync(fd);try{fs.unlinkSync(lock)}catch{}}}\nfunction scan(){\n  fs.mkdirSync(INPUT,{recursive:true});\n  const state=readJson(STATE,{version:1,records:{}}); state.records??={};\n  const folders=fs.readdirSync(INPUT,{withFileTypes:true}).filter(x=>x.isDirectory()).map(x=>x.name);\n  const items=[];\n  for(const folder of folders){\n    const dir=path.join(INPUT,folder);\n    const files=fs.readdirSync(dir,{withFileTypes:true}).filter(x=>x.isFile()).map(x=>x.name);\n    const slides=files.filter(x=>/\\.(png|jpe?g|webp)$/i.test(x)).sort((a,b)=>a.localeCompare(b,'pt-BR',{numeric:true}));\n    const assetVersion=Math.max(0,...slides.map(file=>Math.floor(fs.statSync(path.join(dir,file)).mtimeMs)));\n    const captionFile=files.find(x=>x.toLowerCase()==='texto.txt');\n    const originalCaption=captionFile?safeText(fs.readFileSync(path.join(dir,captionFile),'utf8')):'';\n    const id=slug(folder); const old=state.records[id]||{};\n    const status=slides.length<1||!originalCaption&&!old.caption?'incompleto':allowedStatuses.has(old.status)?old.status:'pendente';\n    items.push({id,folder,title:old.title||folder,slides,assetVersion,caption:old.caption??originalCaption,brief:old.brief||'',status,networks:Array.isArray(old.networks)?old.networks:[],scheduleAt:old.scheduleAt||'',updatedAt:old.updatedAt||'',aiDraft:old.aiDraft&&typeof old.aiDraft==='object'?old.aiDraft:null,deliveries:old.deliveries&&typeof old.deliveries==='object'?old.deliveries:{},audit:Array.isArray(old.audit)?old.audit:[]});\n  }\n  return {state,items};\n}\n\nconst {items}=scan(); const publicBase=String($env.SOCIAL_PUBLIC_MEDIA_BASE_URL||'').replace(/\\/+$/,''); const signingSecret=String($env.SOCIAL_MEDIA_SIGNING_SECRET||'');\nif(publicBase&&signingSecret){const expires=Math.floor(Date.now()/1000)+7200; for(const content of items){content.assetUrls={}; for(const file of content.slides){const signature=crypto.createHmac('sha256',signingSecret).update(content.id+':'+file+':'+expires).digest('base64url');content.assetUrls[file]=publicBase+'?id='+encodeURIComponent(content.id)+'&file='+encodeURIComponent(file)+'&exp='+expires+'&sig='+encodeURIComponent(signature)}}}\nconst publishingLive=String($env.SOCIAL_PUBLISH_ENABLED||'').trim().toLowerCase()==='true';\nconst template=\"<!doctype html>\\r\\n<html lang=\\\"pt-BR\\\">\\r\\n<head>\\r\\n  <meta charset=\\\"utf-8\\\">\\r\\n  <meta name=\\\"viewport\\\" content=\\\"width=device-width, initial-scale=1\\\">\\r\\n  <title>Postagem Redes | Central de conte\u00fado</title>\\r\\n  <style>\\r\\n    :root{--bg:#0b1018;--surface:#141c29;--surface-2:#1b2638;--surface-3:#23324a;--line:#31425d;--line-soft:#263650;--text:#f7f9fd;--muted:#9fadc2;--muted-2:#718099;--red:#ed3340;--red-dark:#b92331;--green:#35c58a;--gold:#ffc04c;--blue:#64b5ff;--shadow:0 24px 70px #02050dbb}*{box-sizing:border-box}html{background:var(--bg)}body{margin:0;background:radial-gradient(circle at 12% -10%,#243756 0,transparent 32%),linear-gradient(140deg,#090e16,#101827 56%,#0c111b);font:15px/1.45 Inter,Segoe UI,Arial,sans-serif;color:var(--text);min-height:100vh}button,input,textarea,select{font:inherit}button{cursor:pointer;border:0;border-radius:10px;padding:10px 14px;font-weight:750;transition:.18s ease}button:hover{filter:brightness(1.08);transform:translateY(-1px)}button:disabled{cursor:not-allowed;opacity:.45;transform:none;filter:none}.top{padding:27px max(24px,calc((100vw - 1480px)/2));border-bottom:1px solid var(--line-soft);background:linear-gradient(180deg,#121b2af5,#111927e8);backdrop-filter:blur(14px);position:sticky;top:0;z-index:2}.topbar{display:flex;justify-content:space-between;gap:18px;align-items:center}.brand{display:flex;align-items:center;gap:14px}.mark{height:46px;width:46px;display:grid;place-items:center;border-radius:14px;background:linear-gradient(145deg,#ff5862,#d71f32 62%,#a9162b);box-shadow:inset 0 1px 1px #ffffff55,0 9px 22px #bc1e3038;font-size:21px;font-weight:950;letter-spacing:-1px}.title h1{font-size:22px;letter-spacing:-.45px;margin:0}.title p{color:var(--muted);margin:2px 0 0}.header-actions{display:flex;align-items:center;gap:10px}.env{display:flex;align-items:center;gap:7px;color:#ffe1a3;background:#3a2b13;border:1px solid #745421;border-radius:999px;padding:7px 10px;font-size:12px;font-weight:750}.env i{width:7px;height:7px;border-radius:50%;background:var(--gold);box-shadow:0 0 10px var(--gold)}.env.live{color:#b7f7d3;background:#163b2d;border-color:#2c8a62}.env.live i{background:#58d68d;box-shadow:0 0 10px #58d68d}.notice{margin-top:19px;display:flex;gap:10px;align-items:flex-start;padding:11px 14px;border:1px solid #6c5222;background:linear-gradient(90deg,#342812,#292213);color:#ffe1a3;border-radius:10px}.notice.live{border-color:#2c8a62;background:linear-gradient(90deg,#143728,#102b20);color:#b7f7d3}.notice strong{color:#fff0c8}.notice.live strong{color:#e6fff0}.shell{max-width:1480px;margin:0 auto;padding:25px 24px 42px}.section-head{display:flex;justify-content:space-between;gap:16px;align-items:end;margin-bottom:16px}.eyebrow{color:var(--blue);font-size:12px;font-weight:850;letter-spacing:.08em;text-transform:uppercase}.section-head h2{font-size:24px;letter-spacing:-.5px;margin:2px 0 0}.section-head p{color:var(--muted);margin:2px 0 0}.toolbar{display:grid;grid-template-columns:minmax(240px,1fr) 165px 170px auto;gap:10px;align-items:center;margin-bottom:17px}.field,.toolbar input,.toolbar select,.form input,.form textarea,.form select{width:100%;background:#0d1420;border:1px solid var(--line);border-radius:10px;color:var(--text);padding:11px 12px;outline:none;transition:.18s}.toolbar input:focus,.toolbar select:focus,.form input:focus,.form textarea:focus,.form select:focus{border-color:#5b94d4;box-shadow:0 0 0 3px #4f94e022}.primary{background:linear-gradient(135deg,#f04451,#d72031);box-shadow:0 8px 18px #d41d2e30;color:#fff}.secondary{background:#2c3b54;color:white}.soft{background:#1c2a3e;color:#d5e6ff;border:1px solid #344a68}.ghost{background:transparent;color:#cad4e5;border:1px solid var(--line)}.danger{background:#48222c;color:#ffd6db;border:1px solid #7c3441}.stats{display:flex;gap:8px;flex-wrap:wrap}.stat{display:flex;align-items:center;gap:6px;background:var(--surface);border:1px solid var(--line-soft);padding:8px 10px;border-radius:9px;color:var(--muted);font-size:13px}.stat b{color:white;font-size:14px}.stat.pulse b{color:#ffdd93}.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(286px,1fr));gap:17px}.card{display:flex;flex-direction:column;background:linear-gradient(160deg,#172131,#131b29);border:1px solid var(--line-soft);border-radius:15px;overflow:hidden;box-shadow:0 10px 30px #0002;transition:.2s}.card:hover{border-color:#55729a;box-shadow:0 18px 39px #0005;transform:translateY(-3px)}.thumb{height:220px;background:#0a0f17;position:relative;display:grid;place-items:center;overflow:hidden}.thumb img{width:100%;height:100%;object-fit:cover;transition:.3s}.card:hover .thumb img{transform:scale(1.025)}.status{position:absolute;left:11px;top:11px;padding:5px 9px;border-radius:999px;background:#192337ed;border:1px solid #34445f;font-size:11px;font-weight:900;letter-spacing:.04em;text-transform:uppercase}.status.pendente{color:#ffe1a3}.status.aprovado,.status.agendado{color:#a7f3d0}.status.rejeitado,.status.incompleto,.status.falhou{color:#ffb1b7}.status.parcial{color:#fde68a}.slide-count{position:absolute;right:11px;bottom:11px;background:#08101ddd;border:1px solid #3b4a61;border-radius:999px;padding:5px 9px;font-size:12px;font-weight:750}.cardbody{display:flex;flex:1;flex-direction:column;padding:15px}.card h3{font-size:16px;line-height:1.28;letter-spacing:-.15px;margin:0 0 8px}.meta{color:var(--muted);font-size:12px}.caption{height:61px;overflow:hidden;color:#d2dae7;margin:10px 0;display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical}.chips{display:flex;gap:5px;flex-wrap:wrap;margin-top:auto}.chip{background:#21324c;color:#acd5ff;border:1px solid transparent;border-radius:999px;padding:4px 7px;font-size:11px;font-weight:800}.chip.ok{background:#163b2d;color:#b7f7d3;border-color:#2c8a62}.chip.fail{background:#48212b;color:#ffc4ca;border-color:#9d4253}.chip.waiting{background:#49391a;color:#ffe49a;border-color:#9e7827}.chip.pending{background:#21324c;color:#acd5ff;border-color:#365276}.chip.none{background:#242b39;color:#acb8c9}.cardfoot{display:flex;justify-content:space-between;align-items:center;gap:9px;margin-top:12px}.updated{color:var(--muted-2);font-size:11px}.empty{grid-column:1/-1;text-align:center;padding:72px 20px;border:1px dashed #3d4c63;border-radius:15px;color:var(--muted);background:#111927}.empty h3{margin:0 0 6px;color:white}.dialog{position:fixed;inset:0;background:#050911c4;backdrop-filter:blur(5px);display:none;align-items:center;justify-content:center;padding:18px;z-index:10}.dialog.open{display:flex}.modal{width:min(1420px,100%);max-height:94vh;overflow:auto;border:1px solid #435675;background:linear-gradient(145deg,#172131,#111925);border-radius:18px;padding:22px;box-shadow:var(--shadow)}.modal.compact{width:min(1020px,100%)}.modalhead{display:flex;justify-content:space-between;gap:12px;align-items:start;padding-bottom:16px;border-bottom:1px solid var(--line-soft)}.modal h2{margin:0;font-size:22px;letter-spacing:-.45px}.modalhead p{margin:3px 0 0;color:var(--muted);font-size:13px}.editor-layout{display:grid;grid-template-columns:minmax(0,1.34fr) minmax(360px,.66fr);gap:22px;margin-top:18px}.media-pane{min-width:0}.media-topline{display:flex;justify-content:space-between;align-items:center;margin-bottom:10px;color:var(--muted);font-size:13px}.media-topline b{color:#dce8f7}.preview-stage{height:min(61vh,670px);background:#080d15;border:1px solid #263650;border-radius:14px;overflow:hidden;position:relative;display:grid;place-items:center}.preview-stage img{width:100%;height:100%;object-fit:contain}.nav-slide{position:absolute;top:50%;transform:translateY(-50%);width:42px;height:42px;border-radius:50%;padding:0;background:#101a2bd9;color:#fff;border:1px solid #41536e;font-size:24px}.nav-slide.prev{left:13px}.nav-slide.next{right:13px}.slide-strip{display:flex;gap:8px;overflow:auto;padding:12px 2px 2px}.slide-tile{position:relative;flex:0 0 80px;height:94px;padding:0;background:#0c1320;border:2px solid transparent;border-radius:9px;overflow:hidden}.slide-tile.active{border-color:var(--red);box-shadow:0 0 0 2px #e72d3a33}.slide-tile img{width:100%;height:100%;object-fit:cover}.slide-number{position:absolute;left:4px;top:4px;border-radius:5px;background:#07101cdd;padding:1px 5px;font-size:10px}.orderbar{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-top:13px;padding:11px;border:1px solid var(--line-soft);background:#101927;border-radius:11px}.orderbar p{margin:0;color:var(--muted);font-size:12px}.order-controls{display:flex;gap:6px}.iconbtn{min-width:34px;padding:7px 9px}.inspector{display:flex;flex-direction:column;min-width:0}.form{display:grid;gap:12px}.form label{display:grid;gap:6px;color:#d2dbe9;font-size:12px;font-weight:800}.form textarea{resize:vertical;min-height:142px;line-height:1.48}.caption-head{display:flex;justify-content:space-between;gap:8px;align-items:center}.counter{color:var(--muted);font-weight:650}.counter.warn{color:#ffd483}.checks{display:grid;grid-template-columns:1fr 1fr;gap:8px}.checks label{display:flex;gap:8px;align-items:center;background:#0e1623;border:1px solid var(--line);border-radius:9px;padding:9px;font-size:13px}.checks label:has(input:checked){border-color:#4b7eaf;background:#152740}.network-icon{display:inline-grid;place-items:center;width:20px;height:20px;border-radius:6px;background:#263a58;color:#b9dcff;font-size:10px;font-weight:950}.schedule-row{display:grid;grid-template-columns:1fr 1fr;gap:10px}.status-help{padding:9px 10px;background:#102033;border:1px solid #294763;border-radius:8px;color:#b7d9f6;font-size:12px}.audit{margin-top:14px;border-top:1px solid var(--line-soft);padding-top:13px}.audit h4{margin:0 0 8px;font-size:13px}.audit-list{display:grid;gap:8px}.audit-item{border-left:2px solid #497bac;padding:2px 0 2px 9px;color:var(--muted);font-size:12px}.audit-item b{color:#e0e9f7}.sticky-actions{position:sticky;bottom:-22px;margin:16px -22px -22px;padding:14px 22px;background:#142033f2;border-top:1px solid #31435e;display:flex;gap:9px;align-items:center;flex-wrap:wrap}.sticky-actions .hint{margin-left:auto;color:var(--muted);font-size:11px}.upload-layout{display:grid;grid-template-columns:minmax(0,1fr) minmax(320px,.85fr);gap:22px;margin-top:18px}.dropzone{display:grid;place-items:center;text-align:center;min-height:154px;padding:18px;border:1px dashed #527ba8;background:linear-gradient(135deg,#122239,#102033);border-radius:13px;color:#bcdafa;transition:.18s}.dropzone.drag{border-color:#ff6871;background:#35202a}.dropzone strong{display:block;color:white;font-size:15px}.dropzone span{font-size:12px;color:var(--muted);margin-top:4px}.dropzone input{display:none}.upload-preview{min-height:246px;background:#0a101a;border:1px solid var(--line-soft);border-radius:13px;display:grid;place-items:center;overflow:hidden}.upload-preview img{width:100%;height:100%;max-height:320px;object-fit:contain}.placeholder-preview{color:var(--muted);text-align:center;padding:20px}.file-list{display:grid;gap:7px;margin-top:11px;max-height:250px;overflow:auto;padding-right:2px}.file-item{display:grid;grid-template-columns:28px 44px 1fr auto;gap:8px;align-items:center;background:#101927;border:1px solid var(--line-soft);border-radius:9px;padding:6px}.file-item img{width:44px;height:44px;object-fit:cover;border-radius:6px}.file-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:#d9e3f0;font-size:12px}.file-actions{display:flex;gap:4px}.file-actions button{padding:5px 7px;min-width:27px}.upload-tip{padding:10px 11px;border-left:3px solid var(--blue);background:#112237;color:#c5dff7;border-radius:0 8px 8px 0;font-size:12px}.toast{position:fixed;right:20px;bottom:20px;max-width:420px;background:#213a57;border:1px solid #5483b2;padding:13px 16px;border-radius:10px;display:none;z-index:20;box-shadow:0 16px 36px #0008}.toast.error{background:#48242c;border-color:#9a3c4a}.toast.show{display:block}.hidden{display:none!important}.sr{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}@media(max-width:940px){.editor-layout,.upload-layout{grid-template-columns:1fr}.preview-stage{height:min(55vh,590px)}.inspector{border-top:1px solid var(--line-soft);padding-top:18px}.toolbar{grid-template-columns:1fr 1fr}.toolbar input{grid-column:1/-1}.stats{grid-column:1/-1}.top{position:static}}@media(max-width:640px){.top,.shell{padding-left:15px;padding-right:15px}.topbar,.section-head{align-items:flex-start;flex-direction:column}.header-actions{width:100%;justify-content:space-between}.env{display:none}.toolbar{grid-template-columns:1fr}.grid{grid-template-columns:1fr}.thumb{height:238px}.modal{padding:15px;border-radius:13px}.modalhead h2{font-size:19px}.preview-stage{height:48vh}.slide-tile{flex-basis:65px;height:78px}.checks,.schedule-row{grid-template-columns:1fr}.sticky-actions{bottom:-15px;margin:16px -15px -15px;padding:12px 15px}.sticky-actions .hint{width:100%;margin:0}.notice{font-size:13px}.stat{font-size:12px}}\\r\\n    /* Ajustes de densidade: o editor n\u00e3o deve cobrir os pr\u00f3prios campos. */\\r\\n    .load-more{grid-column:1/-1;display:flex;justify-content:center;padding-top:4px}\\r\\n    .modal{max-height:92vh}\\r\\n    .editor-layout{gap:18px;padding-bottom:78px}\\r\\n    .preview-stage{height:min(45vh,480px)}\\r\\n    .sticky-actions{position:sticky;bottom:-22px;margin:16px -22px -22px;padding:14px 22px;background:#142033f2}\\r\\n    @media(max-width:940px){.preview-stage{height:min(48vh,480px)}}\\r\\n    @media(max-width:640px){.sticky-actions{bottom:-15px;margin:16px -15px -15px;padding:12px 15px}}\\r\\n  </style>\\r\\n</head>\\r\\n<body>\\r\\n  <header class=\\\"top\\\">\\r\\n    <div class=\\\"topbar\\\">\\r\\n      <div class=\\\"brand\\\"><div class=\\\"mark\\\">P</div><div class=\\\"title\\\"><h1>Postagem Redes</h1><p>Central de conte\u00fado, aprova\u00e7\u00e3o e fila de publica\u00e7\u00e3o</p></div></div>\\r\\n      <div class=\\\"header-actions\\\"><span class=\\\"env\\\" id=\\\"publishMode\\\"><i></i> Homologa\u00e7\u00e3o</span><button class=\\\"primary\\\" onclick=\\\"openUpload()\\\">\uff0b Criar publica\u00e7\u00e3o</button></div>\\r\\n    </div>\\r\\n    <div class=\\\"notice\\\" id=\\\"publishNotice\\\"><strong>Publica\u00e7\u00e3o externa protegida.</strong><span>Aprovar organiza a fila; nenhuma rede ser\u00e1 acionada at\u00e9 que as contas e credenciais sejam validadas.</span></div>\\r\\n  </header>\\r\\n  <main class=\\\"shell\\\">\\r\\n    <div class=\\\"section-head\\\"><div><div class=\\\"eyebrow\\\">Biblioteca de conte\u00fado</div><h2>O que est\u00e1 pronto para revis\u00e3o?</h2><p>Crie uma nova publica\u00e7\u00e3o ou abra qualquer card para revisar o carrossel, a legenda e o destino.</p></div><div class=\\\"stats\\\" id=\\\"stats\\\"></div></div>\\r\\n    <div class=\\\"toolbar\\\"><input id=\\\"search\\\" aria-label=\\\"Buscar conte\u00fado\\\" placeholder=\\\"Buscar por tema, legenda ou status\\\" oninput=\\\"render(true)\\\"><select id=\\\"filter\\\" aria-label=\\\"Filtrar por status\\\" onchange=\\\"render(true)\\\"><option value=\\\"\\\">Todos os status</option><option value=\\\"pendente\\\">Pendentes</option><option value=\\\"aprovado\\\">Aprovados</option><option value=\\\"agendado\\\">Agendados</option><option value=\\\"rejeitado\\\">Rejeitados</option><option value=\\\"incompleto\\\">Incompletos</option></select><select id=\\\"sort\\\" aria-label=\\\"Ordenar conte\u00fados\\\" onchange=\\\"render(true)\\\"><option value=\\\"recent\\\">Atualizados primeiro</option><option value=\\\"old\\\">Mais antigos primeiro</option><option value=\\\"title\\\">T\u00edtulo de A a Z</option><option value=\\\"status\\\">Status</option></select><button class=\\\"ghost\\\" onclick=\\\"location.reload()\\\">\u21bb Atualizar biblioteca</button></div>\\r\\n    <section class=\\\"grid\\\" id=\\\"grid\\\" aria-live=\\\"polite\\\"></section>\\r\\n  </main>\\r\\n\\r\\n  <div class=\\\"dialog\\\" id=\\\"detailDialog\\\" role=\\\"dialog\\\" aria-modal=\\\"true\\\" aria-labelledby=\\\"detailTitle\\\">\\r\\n    <div class=\\\"modal\\\"><div class=\\\"modalhead\\\"><div><div class=\\\"eyebrow\\\">Editor de publica\u00e7\u00e3o</div><h2 id=\\\"detailTitle\\\"></h2><p id=\\\"detailMeta\\\"></p></div><button class=\\\"ghost\\\" onclick=\\\"closeDialog('detailDialog')\\\">Fechar</button></div>\\r\\n      <div class=\\\"editor-layout\\\"><section class=\\\"media-pane\\\"><div class=\\\"media-topline\\\"><span id=\\\"slidePosition\\\">Slide 1 de 1</span><b id=\\\"orderDirty\\\"></b></div><div class=\\\"preview-stage\\\"><button class=\\\"nav-slide prev\\\" type=\\\"button\\\" aria-label=\\\"Slide anterior\\\" onclick=\\\"changeSlide(-1)\\\">\u2039</button><img id=\\\"bigImage\\\" alt=\\\"Pr\u00e9via do carrossel\\\"><button class=\\\"nav-slide next\\\" type=\\\"button\\\" aria-label=\\\"Pr\u00f3ximo slide\\\" onclick=\\\"changeSlide(1)\\\">\u203a</button></div><div class=\\\"slide-strip\\\" id=\\\"slides\\\"></div><div class=\\\"orderbar\\\"><p>Use as setas em cada miniatura para definir a ordem final do carrossel.</p><div class=\\\"order-controls\\\"><button class=\\\"soft iconbtn\\\" type=\\\"button\\\" onclick=\\\"moveSlide(activeSlide,-1)\\\" aria-label=\\\"Mover slide atual para esquerda\\\">\u2190</button><button class=\\\"soft iconbtn\\\" type=\\\"button\\\" onclick=\\\"moveSlide(activeSlide,1)\\\" aria-label=\\\"Mover slide atual para direita\\\">\u2192</button><button class=\\\"secondary\\\" id=\\\"saveOrder\\\" type=\\\"button\\\" onclick=\\\"persistOrder()\\\" disabled>Salvar ordem</button></div></div></section>\\r\\n      <aside class=\\\"inspector\\\"><form class=\\\"form\\\" id=\\\"contentForm\\\"><input type=\\\"hidden\\\" name=\\\"action\\\" value=\\\"save\\\"><input type=\\\"hidden\\\" name=\\\"contentId\\\" id=\\\"contentId\\\"><label>Respons\u00e1vel pela decis\u00e3o<input name=\\\"operator\\\" id=\\\"operator\\\" required maxlength=\\\"80\\\" placeholder=\\\"Digite seu nome\\\"></label><label>T\u00edtulo interno da publica\u00e7\u00e3o<input name=\\\"title\\\" id=\\\"editTitle\\\" required maxlength=\\\"120\\\" placeholder=\\\"Ex.: Visita t\u00e9cnica em obra\\\"></label><label><span class=\\\"caption-head\\\">Legenda <span class=\\\"counter\\\" id=\\\"captionCount\\\">0 caracteres</span></span><textarea name=\\\"caption\\\" id=\\\"caption\\\" required maxlength=\\\"5000\\\" placeholder=\\\"Escreva o texto que ser\u00e1 publicado\\\"></textarea></label><label>Brief para sugest\u00e3o de IA <span class=\\\"counter\\\">(opcional)</span><textarea name=\\\"brief\\\" id=\\\"brief\\\" rows=\\\"3\\\" maxlength=\\\"1600\\\" placeholder=\\\"Ex.: p\u00fablico, objetivo, CTA, restri\u00e7\u00f5es e tom desejado\\\"></textarea></label><label>Redes de destino</label><div class=\\\"checks\\\"><label><input type=\\\"checkbox\\\" name=\\\"network\\\" value=\\\"instagram\\\"><span class=\\\"network-icon\\\">IG</span> Instagram</label><label><input type=\\\"checkbox\\\" name=\\\"network\\\" value=\\\"facebook\\\"><span class=\\\"network-icon\\\">f</span> Facebook</label><label><input type=\\\"checkbox\\\" name=\\\"network\\\" value=\\\"linkedin\\\"><span class=\\\"network-icon\\\">in</span> LinkedIn</label><label><input type=\\\"checkbox\\\" name=\\\"network\\\" value=\\\"x\\\"><span class=\\\"network-icon\\\">X</span> X / thread</label></div><div class=\\\"schedule-row\\\"><label>Status<select name=\\\"status\\\" id=\\\"status\\\" onchange=\\\"syncSchedule()\\\"><option value=\\\"pendente\\\">Manter pendente</option><option value=\\\"aprovado\\\">Aprovar para fila imediata</option><option value=\\\"agendado\\\">Aprovar e agendar</option><option value=\\\"rejeitado\\\">Rejeitar</option></select></label><label id=\\\"scheduleField\\\">Agendar para<input type=\\\"datetime-local\\\" name=\\\"scheduleAt\\\" id=\\\"scheduleAt\\\"></label></div><div class=\\\"status-help\\\" id=\\\"statusHelp\\\"></div><label>Coment\u00e1rio da decis\u00e3o <span class=\\\"counter\\\">(opcional)</span><textarea name=\\\"comment\\\" id=\\\"comment\\\" rows=\\\"3\\\" maxlength=\\\"1000\\\" placeholder=\\\"Ex.: revisar a chamada do slide 1 antes de publicar\\\"></textarea></label></form><section class=\\\"audit\\\" id=\\\"aiDraftPanel\\\" hidden><h4>Sugest\u00e3o da IA</h4><div class=\\\"audit-list\\\" id=\\\"aiDraft\\\"></div></section><section class=\\\"audit\\\"><h4>Hist\u00f3rico recente</h4><div class=\\\"audit-list\\\" id=\\\"auditList\\\"></div></section></aside></div>\\r\\n      <div class=\\\"sticky-actions\\\"><button class=\\\"soft\\\" id=\\\"generateAi\\\" type=\\\"button\\\" onclick=\\\"generateAi()\\\">Gerar sugest\u00e3o IA</button><button class=\\\"primary\\\" type=\\\"button\\\" onclick=\\\"submitContent()\\\">Salvar atualiza\u00e7\u00e3o</button><button class=\\\"danger\\\" type=\\\"button\\\" onclick=\\\"markRejected()\\\">Rejeitar conte\u00fado</button><span class=\\\"hint\\\">A IA prop\u00f5e; a aprova\u00e7\u00e3o humana continua obrigat\u00f3ria. A publica\u00e7\u00e3o externa permanece bloqueada at\u00e9 a homologa\u00e7\u00e3o.</span></div>\\r\\n    </div>\\r\\n  </div>\\r\\n\\r\\n  <div class=\\\"dialog\\\" id=\\\"uploadDialog\\\" role=\\\"dialog\\\" aria-modal=\\\"true\\\" aria-labelledby=\\\"uploadTitle\\\">\\r\\n    <div class=\\\"modal compact\\\"><div class=\\\"modalhead\\\"><div><div class=\\\"eyebrow\\\">Nova publica\u00e7\u00e3o</div><h2 id=\\\"uploadTitle\\\">Monte o carrossel antes de enviar</h2><p>Adicione de 1 a 10 imagens, ajuste a ordem, escreva a legenda e envie para aprova\u00e7\u00e3o.</p></div><button class=\\\"ghost\\\" onclick=\\\"closeDialog('uploadDialog')\\\">Fechar</button></div>\\r\\n      <form id=\\\"uploadForm\\\" class=\\\"upload-layout\\\"><section class=\\\"form\\\"><label>Respons\u00e1vel pelo envio<input name=\\\"operator\\\" id=\\\"uploadOperator\\\" required maxlength=\\\"80\\\" placeholder=\\\"Digite seu nome\\\"></label><label>T\u00edtulo interno da publica\u00e7\u00e3o<input name=\\\"title\\\" id=\\\"uploadTitleInput\\\" required maxlength=\\\"120\\\" placeholder=\\\"Ex.: Visita t\u00e9cnica em obra\\\"></label><label><span class=\\\"caption-head\\\">Legenda <span class=\\\"counter\\\" id=\\\"uploadCaptionCount\\\">0 caracteres</span></span><textarea name=\\\"caption\\\" id=\\\"uploadCaption\\\" required maxlength=\\\"5000\\\" placeholder=\\\"Escreva o texto que ser\u00e1 publicado nas redes\\\"></textarea></label><div class=\\\"upload-tip\\\">A publica\u00e7\u00e3o entrar\u00e1 como <strong>pendente</strong>. Depois, qualquer pessoa autorizada poder\u00e1 abrir o card, revisar os slides e aprovar/agendar.</div><div class=\\\"actions\\\"><button class=\\\"primary\\\" id=\\\"uploadSubmit\\\" type=\\\"submit\\\">Enviar para aprova\u00e7\u00e3o</button><button class=\\\"ghost\\\" type=\\\"button\\\" onclick=\\\"closeDialog('uploadDialog')\\\">Cancelar</button></div></section><section><label class=\\\"dropzone\\\" id=\\\"dropzone\\\" for=\\\"uploadFiles\\\"><input id=\\\"uploadFiles\\\" type=\\\"file\\\" accept=\\\"image/png,image/jpeg,image/webp\\\" multiple><strong>Arraste imagens aqui ou clique para escolher</strong><span>PNG, JPG/JPEG ou WEBP \u00b7 at\u00e9 10 imagens</span></label><div class=\\\"upload-preview\\\" id=\\\"uploadPreview\\\"><div class=\\\"placeholder-preview\\\">A pr\u00e9via do primeiro slide aparecer\u00e1 aqui.</div></div><div class=\\\"media-topline\\\" style=\\\"margin:12px 0 0\\\"><span id=\\\"uploadCount\\\">Nenhuma imagem selecionada</span><b>Ordem do carrossel</b></div><div class=\\\"file-list\\\" id=\\\"fileList\\\"></div></section></form>\\r\\n    </div>\\r\\n  </div>\\r\\n  <div class=\\\"toast\\\" id=\\\"toast\\\" role=\\\"status\\\" aria-live=\\\"polite\\\"></div>\\r\\n  <script>\\r\\n    const CONTENTS=__POSTAGEM_REDES_DATA__;\\r\\n    const asset=(id,file,version='')=>CONTENTS.find(content=>content.id===id)?.assetUrls?.[file]||`${location.origin}/webhook/postagem-redes-arquivo?id=${encodeURIComponent(id)}&file=${encodeURIComponent(file)}&v=${encodeURIComponent(version)}`;\\r\\n    const api=`${location.origin}/webhook/postagem-redes-api`;\\r\\n    const PUBLISHING_LIVE=__POSTAGEM_REDES_MODE__;\\r\\n    const STATUS={pendente:'Pendente',aprovado:'Aprovado',agendado:'Agendado',rejeitado:'Rejeitado',incompleto:'Incompleto',publicado:'Publicado',parcial:'Publicado parcialmente',falhou:'Falhou'};\\r\\n    const HELP={pendente:'O conte\u00fado continua vis\u00edvel para revis\u00e3o e n\u00e3o segue para publica\u00e7\u00e3o.',aprovado:'O conte\u00fado ficar\u00e1 pronto na fila de publica\u00e7\u00e3o para as redes selecionadas.',agendado:'Defina data e hora; o conte\u00fado entrar\u00e1 na fila de publica\u00e7\u00e3o no hor\u00e1rio indicado.',rejeitado:'O conte\u00fado ser\u00e1 mantido no hist\u00f3rico como rejeitado; informe o motivo no coment\u00e1rio.'};\\r\\n    const publishMode=document.querySelector('#publishMode');const publishNotice=document.querySelector('#publishNotice');if(PUBLISHING_LIVE){publishMode.classList.add('live');publishMode.innerHTML='<i></i> Publica\u00e7\u00e3o ativa';publishNotice.classList.add('live');publishNotice.innerHTML='<strong>Publica\u00e7\u00e3o externa ativa.</strong><span>Conte\u00fados aprovados ou agendados seguem para as redes selecionadas. Confira o resultado individual em cada card.</span>'}let selected=null,activeSlide=0,slideOrder=[],uploadFiles=[],uploadPreviewIndex=0,visibleLimit=8;\\r\\n    const esc=s=>String(s??'').replace(/[&<>\\\"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','\\\"':'&quot;',\\\"'\\\":'&#39;'}[c]));\\r\\n    const label=s=>STATUS[s]||s;\\r\\n    const date=v=>v?new Date(v).toLocaleString('pt-BR',{dateStyle:'short',timeStyle:'short'}):'novo';\\r\\n    const remember=(key,value)=>{try{localStorage.setItem(key,value)}catch{}};\\r\\n    const recalled=key=>{try{return localStorage.getItem(key)||''}catch{return ''}};\\r\\n    function networkChips(networks,deliveries={}){if(!networks?.length)return '<span class=\\\"chip none\\\">redes n\u00e3o definidas</span>';return networks.map(network=>{const delivery=deliveries?.[network]||{};const state=String(delivery.status||'queued');const tone=state==='published'?'ok':(state==='failed'||state==='blocked')?'fail':(state==='retry'||state==='dispatching')?'waiting':'pending';const outcome=state==='published'?'publicado':state==='failed'?'falhou':state==='blocked'?'n\u00e3o configurado':state==='retry'?'nova tentativa':state==='dispatching'?'enviando':'na fila';const title=delivery.lastError?` title=\\\"${esc(delivery.lastError)}\\\"`:'';return `<span class=\\\"chip ${tone}\\\"${title}>${esc(network==='x'?'X / thread':network)} \u00b7 ${outcome}</span>`;}).join('')} function sortRows(rows){const mode=document.querySelector('#sort').value;return rows.sort((a,b)=>{if(mode==='title')return a.title.localeCompare(b.title,'pt-BR');if(mode==='status')return a.status.localeCompare(b.status,'pt-BR');const av=Date.parse(a.updatedAt||0),bv=Date.parse(b.updatedAt||0);return mode==='old'?av-bv:bv-av})}\\r\\n    function render(reset=false){\\r\\n      if(reset) visibleLimit=8;\\r\\n      const q=document.querySelector('#search').value.toLowerCase();\\r\\n      const filter=document.querySelector('#filter').value;\\r\\n      const rows=sortRows(CONTENTS.filter(x=>(!filter||x.status===filter)&&(`${x.title} ${x.caption} ${x.status} ${(x.networks||[]).join(' ')}`).toLowerCase().includes(q)));\\r\\n      const counts=CONTENTS.reduce((a,x)=>(a[x.status]=(a[x.status]||0)+1,a),{});\\r\\n      document.querySelector('#stats').innerHTML=`<span class=\\\"stat\\\"><b>${CONTENTS.length}</b> conte\u00fados</span><span class=\\\"stat pulse\\\"><b>${counts.pendente||0}</b> pendentes</span><span class=\\\"stat\\\"><b>${counts.aprovado||0}</b> aprovados</span><span class=\\\"stat\\\"><b>${counts.agendado||0}</b> agendados</span>`;\\r\\n      if(!rows.length){document.querySelector('#grid').innerHTML='<div class=\\\"empty\\\"><h3>Nenhum conte\u00fado encontrado</h3><p>Ajuste a busca/filtro ou crie uma nova publica\u00e7\u00e3o.</p><button class=\\\"primary\\\" onclick=\\\"openUpload()\\\">Criar publica\u00e7\u00e3o</button></div>';return}\\r\\n      const visible=rows.slice(0,visibleLimit);\\r\\n      const cards=visible.map(x=>`<article class=\\\"card\\\"><div class=\\\"thumb\\\">${x.slides[0]?`<img src=\\\"${asset(x.id,x.slides[0],x.assetVersion)}\\\" loading=\\\"lazy\\\" decoding=\\\"async\\\" fetchpriority=\\\"low\\\" alt=\\\"${esc(x.title)}\\\">`:''}<span class=\\\"status ${esc(x.status)}\\\">${label(x.status)}</span><span class=\\\"slide-count\\\">${x.slides.length} ${x.slides.length===1?'slide':'slides'}</span></div><div class=\\\"cardbody\\\"><h3>${esc(x.title)}</h3><div class=\\\"meta\\\">${x.updatedAt?`Atualizado ${date(x.updatedAt)}`:'Aguardando primeira revis\u00e3o'}</div><div class=\\\"caption\\\">${esc(x.caption||'Sem legenda \u2014 complete os dados antes de aprovar.')}</div><div class=\\\"chips\\\">${networkChips(x.networks,x.deliveries)}</div><div class=\\\"cardfoot\\\"><span class=\\\"updated\\\">${x.audit?.length||0} ${x.audit?.length===1?'registro':'registros'}</span><button class=\\\"secondary\\\" onclick=\\\"openContent('${x.id}')\\\">Abrir editor</button></div></div></article>`).join('');\\r\\n      const more=rows.length>visible.length?`<div class=\\\"load-more\\\"><button class=\\\"ghost\\\" onclick=\\\"showMore()\\\">Mostrar mais ${Math.min(8,rows.length-visible.length)} de ${rows.length-visible.length}</button></div>`:'';\\r\\n      document.querySelector('#grid').innerHTML=cards+more;\\r\\n    }\\r\\n    function showMore(){visibleLimit+=8;render()}\\r\\n    function openContent(id){selected=CONTENTS.find(x=>x.id===id);if(!selected)return;activeSlide=0;slideOrder=[...selected.slides];document.querySelector('#detailTitle').textContent=selected.title;document.querySelector('#detailMeta').textContent=`${selected.slides.length} slides \u00b7 ${label(selected.status)} \u00b7 ${selected.updatedAt?`atualizado ${date(selected.updatedAt)}`:'novo'}`;document.querySelector('#contentId').value=id;document.querySelector('#operator').value=recalled('postagem-redes-operator');document.querySelector('#editTitle').value=selected.title||'';document.querySelector('#caption').value=selected.caption||'';document.querySelector('#brief').value=selected.brief||'';document.querySelector('#comment').value='';document.querySelector('#status').value=['publicado','parcial','falhou'].includes(selected.status)?'pendente':selected.status;document.querySelector('#scheduleAt').value=selected.scheduleAt?selected.scheduleAt.slice(0,16):'';document.querySelectorAll('[name=network]').forEach(el=>el.checked=(selected.networks||[]).includes(el.value));syncCaptionCount();syncSchedule();renderAudit();renderAiDraft();renderSlides();document.querySelector('#detailDialog').classList.add('open')}\\r\\n    function renderAudit(){const audit=[...(selected.audit||[])].reverse().slice(0,4);document.querySelector('#auditList').innerHTML=audit.length?audit.map(a=>`<div class=\\\"audit-item\\\"><b>${esc(a.operator||'Sistema')}</b> \u00b7 ${esc(label(a.action||''))}<br>${date(a.at)}${a.comment?` \u00b7 ${esc(a.comment)}`:''}</div>`).join(''):'<div class=\\\"audit-item\\\">Ainda n\u00e3o h\u00e1 decis\u00f5es registradas para este conte\u00fado.</div>'}\\r\\n    function renderAiDraft(){const panel=document.querySelector('#aiDraftPanel'),target=document.querySelector('#aiDraft'),draft=selected&&selected.aiDraft;if(!draft){panel.hidden=true;target.innerHTML='';return}const variants=draft.variants||{};const block=(name,value)=>value?`<div class=\\\"audit-item\\\"><b>${esc(name)}</b><br>${esc(Array.isArray(value)?value.join(' '):value)}</div>`:'';target.innerHTML=block('Legenda-base',draft.baseCaption)+block('Instagram',variants.instagram)+block('Facebook',variants.facebook)+block('LinkedIn',variants.linkedin)+block('X / thread',variants.xThread)+(draft.reviewNotes?.length?`<div class=\\\"audit-item\\\"><b>Pontos para revisar</b><br>${draft.reviewNotes.map(esc).join(' \u00b7 ')}</div>`:'')+`<div class=\\\"actions\\\"><button class=\\\"soft\\\" type=\\\"button\\\" onclick=\\\"applyAiDraft()\\\">Usar legenda-base</button></div>`;panel.hidden=false}\\r\\n    function renderSlides(){if(!selected)return;const file=slideOrder[activeSlide]||slideOrder[0];document.querySelector('#bigImage').src=file?asset(selected.id,file,selected.assetVersion):'';document.querySelector('#slidePosition').textContent=`Slide ${activeSlide+1} de ${slideOrder.length}`;const dirty=slideOrder.join('|')!==selected.slides.join('|');document.querySelector('#orderDirty').textContent=dirty?'Ordem alterada \u2014 salve para aplicar.':'';document.querySelector('#saveOrder').disabled=!dirty;document.querySelector('#slides').innerHTML=slideOrder.map((s,i)=>`<div style=\\\"position:relative\\\"><button type=\\\"button\\\" class=\\\"slide-tile ${i===activeSlide?'active':''}\\\" onclick=\\\"showSlide(${i})\\\"><img src=\\\"${asset(selected.id,s,selected.assetVersion)}\\\" loading=\\\"lazy\\\" decoding=\\\"async\\\" alt=\\\"Slide ${i+1}\\\"><span class=\\\"slide-number\\\">${i+1}</span></button><div style=\\\"display:flex;gap:2px;justify-content:center;margin-top:3px\\\"><button class=\\\"ghost iconbtn\\\" style=\\\"padding:3px 6px;min-width:25px\\\" type=\\\"button\\\" onclick=\\\"moveSlide(${i},-1)\\\" ${i===0?'disabled':''} aria-label=\\\"Mover slide ${i+1} para esquerda\\\">\u2190</button><button class=\\\"ghost iconbtn\\\" style=\\\"padding:3px 6px;min-width:25px\\\" type=\\\"button\\\" onclick=\\\"moveSlide(${i},1)\\\" ${i===slideOrder.length-1?'disabled':''} aria-label=\\\"Mover slide ${i+1} para direita\\\">\u2192</button></div></div>`).join('')}\\r\\n    function showSlide(index){activeSlide=Math.max(0,Math.min(index,slideOrder.length-1));renderSlides()}function changeSlide(delta){showSlide(activeSlide+delta)}function moveSlide(index,delta){const target=index+delta;if(target<0||target>=slideOrder.length)return;[slideOrder[index],slideOrder[target]]=[slideOrder[target],slideOrder[index]];activeSlide=target;renderSlides()}\\r\\n    function syncCaptionCount(){const len=document.querySelector('#caption').value.length,el=document.querySelector('#captionCount');el.textContent=`${len.toLocaleString('pt-BR')} / 5.000 caracteres`;el.classList.toggle('warn',len>4000)}function syncUploadCaptionCount(){const len=document.querySelector('#uploadCaption').value.length,el=document.querySelector('#uploadCaptionCount');el.textContent=`${len.toLocaleString('pt-BR')} / 5.000 caracteres`;el.classList.toggle('warn',len>4000)}function syncSchedule(){const status=document.querySelector('#status').value,isScheduled=status==='agendado';document.querySelector('#scheduleField').classList.toggle('hidden',!isScheduled);document.querySelector('#scheduleAt').required=isScheduled;document.querySelector('#statusHelp').textContent=HELP[status]||''}\\r\\n    function closeDialog(id){document.querySelector('#'+id).classList.remove('open')}function openUpload(){uploadFiles=[];uploadPreviewIndex=0;document.querySelector('#uploadForm').reset();document.querySelector('#uploadOperator').value=recalled('postagem-redes-operator');syncUploadCaptionCount();renderUploadFiles();document.querySelector('#uploadDialog').classList.add('open')}function markRejected(){document.querySelector('#status').value='rejeitado';syncSchedule();submitContent()}function toast(message,error=false){const t=document.querySelector('#toast');t.textContent=message;t.classList.toggle('error',error);t.classList.add('show');setTimeout(()=>t.classList.remove('show'),4200)}\\r\\n    async function sendJson(payload){const response=await fetch(api,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload)});const data=await response.json().catch(()=>({ok:false,message:'Resposta inv\u00e1lida do servidor.'}));if(!response.ok||!data.ok)throw Error(data.message||'N\u00e3o foi poss\u00edvel salvar.');return data}\\r\\n    function applyAiDraft(){const draft=selected&&selected.aiDraft;if(!draft?.baseCaption)return;document.querySelector('#caption').value=draft.baseCaption;syncCaptionCount();toast('Legenda-base da IA aplicada no editor. Revise e salve a atualiza\u00e7\u00e3o para confirmar.')}async function generateAi(){const operator=document.querySelector('#operator').value.trim();if(!operator){toast('Informe o respons\u00e1vel antes de gerar uma sugest\u00e3o.',true);document.querySelector('#operator').focus();return}const button=document.querySelector('#generateAi');button.disabled=true;button.textContent='Gerando\u2026';try{remember('postagem-redes-operator',operator);const data=await sendJson({action:'generate',contentId:selected.id,operator,brief:document.querySelector('#brief').value});if(!data.ok)throw Error(data.message||'A IA n\u00e3o est\u00e1 dispon\u00edvel.');toast(data.message||'Sugest\u00e3o salva.');setTimeout(()=>location.reload(),650)}catch(error){toast(error.message,true);button.disabled=false;button.textContent='Gerar sugest\u00e3o IA'}}\\r\\n    async function submitContent(){const form=document.querySelector('#contentForm');if(!form.reportValidity())return;const fd=new FormData(form),payload=Object.fromEntries(fd.entries());payload.networks=JSON.stringify([...form.querySelectorAll('[name=network]:checked')].map(x=>x.value));try{remember('postagem-redes-operator',payload.operator);const data=await sendJson(payload);toast(data.message||'Atualiza\u00e7\u00e3o salva.');setTimeout(()=>location.reload(),650)}catch(error){toast(error.message,true)}}\\r\\n    async function persistOrder(){const operator=document.querySelector('#operator').value.trim();if(!operator){toast('Informe o respons\u00e1vel antes de salvar a ordem.',true);document.querySelector('#operator').focus();return}try{remember('postagem-redes-operator',operator);const data=await sendJson({action:'reorder',contentId:selected.id,operator,slides:JSON.stringify(slideOrder)});toast(data.message||'Ordem atualizada.');setTimeout(()=>location.reload(),650)}catch(error){toast(error.message,true)}}\\r\\n    function addUploadFiles(files){const images=[...files].filter(f=>['image/png','image/jpeg','image/webp'].includes(f.type));if(images.length!==files.length)toast('Foram ignorados arquivos fora de PNG, JPG/JPEG ou WEBP.',true);const available=10-uploadFiles.length;if(images.length>available)toast(`Somente ${available} imagem(ns) foram adicionadas; o limite \u00e9 10.`,true);uploadFiles.push(...images.slice(0,Math.max(0,available)));renderUploadFiles()}\\r\\n    function renderUploadFiles(){document.querySelector('#uploadCount').textContent=uploadFiles.length?`${uploadFiles.length} de 10 imagem(ns) selecionada(s)`:'Nenhuma imagem selecionada';const preview=document.querySelector('#uploadPreview');const current=uploadFiles[uploadPreviewIndex];preview.innerHTML=current?`<img src=\\\"${URL.createObjectURL(current)}\\\" alt=\\\"Pr\u00e9via do slide ${uploadPreviewIndex+1}\\\">`:'<div class=\\\"placeholder-preview\\\">A pr\u00e9via do primeiro slide aparecer\u00e1 aqui.</div>';document.querySelector('#fileList').innerHTML=uploadFiles.length?uploadFiles.map((f,i)=>`<div class=\\\"file-item\\\"><b>${String(i+1).padStart(2,'0')}</b><button type=\\\"button\\\" style=\\\"padding:0;background:none\\\" onclick=\\\"setUploadPreview(${i})\\\"><img src=\\\"${URL.createObjectURL(f)}\\\" alt=\\\"Slide ${i+1}\\\"></button><span class=\\\"file-name\\\" title=\\\"${esc(f.name)}\\\">${esc(f.name)}</span><span class=\\\"file-actions\\\"><button class=\\\"ghost\\\" type=\\\"button\\\" onclick=\\\"moveUpload(${i},-1)\\\" ${i===0?'disabled':''} aria-label=\\\"Mover para cima\\\">\u2191</button><button class=\\\"ghost\\\" type=\\\"button\\\" onclick=\\\"moveUpload(${i},1)\\\" ${i===uploadFiles.length-1?'disabled':''} aria-label=\\\"Mover para baixo\\\">\u2193</button><button class=\\\"danger\\\" type=\\\"button\\\" onclick=\\\"removeUpload(${i})\\\" aria-label=\\\"Remover\\\">\u00d7</button></span></div>`).join(''):'<div class=\\\"small\\\">As imagens escolhidas aparecer\u00e3o aqui para voc\u00ea definir a ordem.</div>'}\\r\\n    function setUploadPreview(index){uploadPreviewIndex=index;renderUploadFiles()}function moveUpload(index,delta){const target=index+delta;if(target<0||target>=uploadFiles.length)return;[uploadFiles[index],uploadFiles[target]]=[uploadFiles[target],uploadFiles[index]];uploadPreviewIndex=target;renderUploadFiles()}function removeUpload(index){uploadFiles.splice(index,1);uploadPreviewIndex=Math.max(0,Math.min(uploadPreviewIndex,uploadFiles.length-1));renderUploadFiles()}\\r\\n    async function submitUpload(event){event.preventDefault();const form=event.currentTarget;if(!form.reportValidity())return;if(!uploadFiles.length){toast('Selecione ao menos uma imagem para o carrossel.',true);return}const operator=document.querySelector('#uploadOperator').value.trim(),title=document.querySelector('#uploadTitleInput').value.trim(),caption=document.querySelector('#uploadCaption').value.trim();const body=new FormData();body.append('action','upload');body.append('operator',operator);body.append('title',title);body.append('caption',caption);const order=[];uploadFiles.forEach((file,index)=>{const key=`slide-${String(index).padStart(2,'0')}`;order.push(key);body.append(key,file,file.name)});body.append('order',JSON.stringify(order));const submit=document.querySelector('#uploadSubmit');submit.disabled=true;submit.textContent='Enviando\u2026';try{const response=await fetch(api,{method:'POST',body});const data=await response.json().catch(()=>({ok:false,message:'Resposta inv\u00e1lida do servidor.'}));if(!response.ok||!data.ok)throw Error(data.message||'N\u00e3o foi poss\u00edvel enviar.');remember('postagem-redes-operator',operator);toast(data.message||'Conte\u00fado enviado para aprova\u00e7\u00e3o.');setTimeout(()=>location.reload(),650)}catch(error){toast(error.message,true);submit.disabled=false;submit.textContent='Enviar para aprova\u00e7\u00e3o'}}\\r\\n    document.querySelector('#caption').addEventListener('input',syncCaptionCount);document.querySelector('#uploadCaption').addEventListener('input',syncUploadCaptionCount);document.querySelector('#contentForm').addEventListener('submit',e=>{e.preventDefault();submitContent()});document.querySelector('#uploadForm').addEventListener('submit',submitUpload);document.querySelector('#uploadFiles').addEventListener('change',e=>addUploadFiles(e.target.files));const drop=document.querySelector('#dropzone');['dragenter','dragover'].forEach(type=>drop.addEventListener(type,e=>{e.preventDefault();drop.classList.add('drag')}));['dragleave','drop'].forEach(type=>drop.addEventListener(type,e=>{e.preventDefault();drop.classList.remove('drag')}));drop.addEventListener('drop',e=>addUploadFiles(e.dataTransfer.files));document.addEventListener('keydown',e=>{if(e.key==='Escape'){closeDialog('detailDialog');closeDialog('uploadDialog')}if(document.querySelector('#detailDialog').classList.contains('open')&&e.key==='ArrowLeft')changeSlide(-1);if(document.querySelector('#detailDialog').classList.contains('open')&&e.key==='ArrowRight')changeSlide(1)});render();\\r\\n  </script>\\r\\n</body>\\r\\n</html>\\r\\n\";\nconst data=JSON.stringify(items).replace(/</g,'\\u003c');\nreturn [{json:{html:template.replace('__POSTAGEM_REDES_MODE__',publishingLive?'true':'false').replace('__POSTAGEM_REDES_DATA__',data)}}];"
      },
      "name": "Processar",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        0,
        0
      ]
    },
    {
      "parameters": {
        "respondWith": "text",
        "responseBody": "={{ $json.html }}",
        "options": {
          "responseHeaders": {
            "entries": [
              {
                "name": "Content-Type",
                "value": "text/html; charset=utf-8"
              },
              {
                "name": "Cache-Control",
                "value": "no-store"
              }
            ]
          }
        }
      },
      "name": "Responder",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.5,
      "position": [
        280,
        0
      ]
    }
  ],
  "connections": {
    "Webhook": {
      "main": [
        [
          {
            "node": "Processar",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Processar": {
      "main": [
        [
          {
            "node": "Responder",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "availableInMCP": false
  },
  "nodeGroups": [],
  "activeVersionId": "e7c3a9d6-a52c-4c05-9d58-a8b18aeedea0",
  "versionCounter": 10,
  "triggerCount": 1,
  "sourceWorkflowId": null,
  "versionMetadata": {
    "name": "Postagem Redes \u2014 Portal Visual",
    "description": null
  },
  "active": false
}
Pro

For the full experience including quality scoring and batch install features for each workflow upgrade to Pro

About this workflow

Postagem Redes — Portal Visual. Webhook trigger; 4 nodes.

Source: https://github.com/Mayconxzdev/PostagemRedes/blob/main/workflows/04-portal-visual.sanitized.json — original creator credit. Request a take-down →

More General workflows → · Browse all categories →

Related workflows

Workflows that share integrations, category, or trigger type with this one. All free to copy and import.

General

A production-ready authentication workflow implementing secure user registration, login, token verification, and refresh token mechanisms. Perfect for adding authentication to any application without

Crypto, Data Table, Execute Workflow Trigger
General

Portfolio Orchestrator. Uses httpRequest. Webhook trigger; 59 nodes.

HTTP Request
General

This n8n template demonstrates how a simple Multi-Layer Perceptron (MLP) neural network can predict housing prices. The prediction is based on four key features, processed through a three-layer model.

General

github code Try yourself

Google Calendar
General

This workflow receives new consult bookings via webhook (Calendly v2 or a generic scheduling tool), generates timed confirmation and reminder messages, runs each SMS through a separate compliance work

Twilio, Email Send