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 →
{
"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=>({'&':'&','<':'<','>':'>','\\\"':'"',\\\"'\\\":'''}[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
}
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 →
Related workflows
Workflows that share integrations, category, or trigger type with this one. All free to copy and import.
A production-ready authentication workflow implementing secure user registration, login, token verification, and refresh token mechanisms. Perfect for adding authentication to any application without
Portfolio Orchestrator. Uses httpRequest. Webhook trigger; 59 nodes.
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.
github code Try yourself
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